start page | rating of books | rating of authors | reviews | copyrights

Book Home Java Enterprise in a Nutshell Search this book

5.2. Printing in Java 1.2

Java 1.2 introduces a more complete printing API. As in Java 1.1, printing is done by calling methods of a special Graphics object that represents the printer device. The printer coordinate system and base resolution of 72 points per inch are the same in both Java 1.1 and Java 1.2. Beyond these similarities, however, the Java 1.2 API flips the Java 1.1 API upside down. Instead of asking a PrintJob object for the Graphics object to draw to, Java 1.2 uses a callback model. You tell the Java 1.2 printing API the object you'd like to print, and it calls the print() method of that object, passing in the appropriate Graphics object to draw to. In Java 1.1, your printing code is in charge of the print job, while in Java 1.2, the print job is in charge of your printing code.

The Java 1.2 printing API is contained in the java.awt.print package. Key classes and interfaces in this package are Printable, which represents a printable object, Pageable, which represents a multipage printable document, and PrinterJob, which coordinates the print job and serves as an intermediary between the Java API and the native printing system. Do not confuse java.awt.print.PrinterJob with the java.awt.PrintJob class used by the Java 1.1 printing API! Another important class is PageFormat, which represents the size of the paper being printed on, its margins, and the printing orientation (i.e., portrait mode or landscape mode).

The basic Java 1.2 printing algorithm includes the following steps:

  1. First, obtain a PrinterJob object to coordinate the printing. Do this by calling the static method PrinterJob.getPrinterJob().

  2. Obtain a PageFormat object that describes the size, margins, and orientation of the page or pages to be printed. The PrinterJob object has methods that allow you to obtain a default PageFormat object and display a dialog asking the user to specify paper, margin, and orientation information. (You might display this dialog box in response to a Print Setup... menu item, for example.)

  3. Next, tell the PrinterJob object what it is that you want to print. The item to print is an object that implements either the Printable interface or the Pageable interface (we'll discuss each of these in more detail shortly). You pass this object to either the setPrintable() or the setPageable() method of the PrinterJob.

  4. Unless you want the printing to occur silently, without any user interaction, your next call is to the printDialog() method of the PrinterJob object. This method displays a dialog box, giving the user the opportunity to specify the printer to use and the number of copies to print. If you are printing a multipage Pageable object, this dialog box allows the user to select a subset of pages to print, rather than printing the entire Pageable document. The dialog box also gives the user the opportunity to cancel the print job. If the printDialog() method returns false, the user has asked to cancel printing and you should not proceed.

  5. Finally, you call the print() method of the PrinterJob. This tells the PrinterJob to begin the printing process.

  6. The PrinterJob is now in control of printing. As we'll discuss later, PrinterJob invokes methods of the Printable or Pageable object you specified, providing the opportunity for your object to print itself to an appropriate Graphics object.

5.2.1. Printing Single-Page Objects

When the object, figure, or document you want to print fits on a single printed page, you typically represent it using the Printable interface. This interface defines a single method, print(), that the PrinterJob calls to print the page. The print() method has three arguments. The first is the Graphics object that represents the printer. print() should do all of its drawing using this object. This Graphics object may be cast to a Graphics2D object, enabling all the features of Java 2D, including the use of floating-point coordinates to position graphics elements with more precision than is possible with integer coordinates.

The second argument to print() is a PageFormat object. Your print() method should call the getImageableX(), getImageableY(), getImageableWidth(), and getImageableHeight() methods of PageFormat to determine the size and position of the area that it should draw in. Note that these methods are poorly named. The values they return represent the page and margin sizes requested by the user, not the size of the paper actually available in the printer or the imageable area of the printer (i.e., the region of the page that a specific type of printer can actually print to).

The third argument is a page number. Although the Printable interface is most useful for single-page documents, it can be used for multipage documents. The PrinterJob has no way to determine how many pages a Printable object requires. Indeed, a Printable object may be implemented in such a way that it does not know how many pages it requires either (e.g., a PrintableStream object that prints a stream of text as it arrives). Because the page count is not known in advance, the PrinterJob calls the print() method repeatedly, incrementing the page number after printing each page.

One important responsibility of the print() method is to notify the PrinterJob when all pages are printed. Your method does this by returning the constant Printable.NO_SUCH_PAGE when the PrinterJob asks it to print a page that is past the end of the document.

It is also important to implement the print() method so that it can be called more than once for each page. As of this writing, Sun's Java 1.2 printing implementation calls the print() method at least twice for each page (we'll see why at the end of this chapter).

Example 5-1 shows a PrintableComponent class that can be used to print the contents of a Swing component, applet, or custom AWT component. This class is a wrapper around a Component and implements the Printable interface. Note that it defines two print() methods. One is the three-argument Printable method I already described. The other print() method takes no arguments and implements the general Java 1.2 printing algorithm. It creates a PrinterJob, displays some dialogs to the user, and initiates the printing process. To print a component, create a PrintableComponent for that component, then call its print() method with no arguments.

Example 5-1. PrintableComponent.java

import java.awt.*;
import java.awt.print.*;

/**
 * This wrapper class encapsulates a Component and allows it to be printed
 * using the Java 1.2 printing API
 */
public class PrintableComponent implements Printable {
  // The component to be printed
  Component c;

  /** Create a PrintableComponent wrapper around a Component */
  public PrintableComponent(Component c) { this.c = c; }

  /**
   * This method is not part of the Printable interface.  It is a method
   * that sets up the PrinterJob and initiates the printing.
   */
  public void print() throws PrinterException {
    // Get the PrinterJob object
    PrinterJob job = PrinterJob.getPrinterJob();
    // Get the default page format, then allow the user to modify it
    PageFormat format = job.pageDialog(job.defaultPage());
    // Tell the PrinterJob what to print
    job.setPrintable(this, format);
    // Ask the user to confirm, and then begin the printing process
    if (job.printDialog()) 
      job.print();
  }

  /**
   * This is the "callback" method that the PrinterJob will invoke.
   * This method is defined by the Printable interface.
   */
  public int print(Graphics g, PageFormat format, int pagenum) {
    // The PrinterJob will keep trying to print pages until we return
    // this value to tell it that it has reached the end
    if (pagenum > 0) 
      return Printable.NO_SUCH_PAGE;

    // We're passed a Graphics object, but it can always be cast to Graphics2D
    Graphics2D g2 = (Graphics2D) g;

    // Use the top and left margins specified in the PageFormat Note
    // that the PageFormat methods are poorly named.  They specify
    // margins, not the actual imageable area of the printer.
    g2.translate(format.getImageableX(), format.getImageableY());

    // Tell the Component to draw itself to the printer by passing in 
    // the Graphics2D object.  This will not work well if the Component
    // has double-buffering enabled.
    c.paint(g2);

    // Return this constant to tell the PrinterJob that we printed the page
    return Printable.PAGE_EXISTS;
  }
}

There are a few important points to note about this PrintableComponent example. First, it is not designed to work with native AWT components, since those components do not do their own drawing. Second, it does not work well for components that use double-buffering because double-buffering locks the component drawing into the relatively low resolution of an off-screen image, rather than taking advantage of the high resolution available on the printer. Finally, PrintableComponent prints only the visible portion of a component, not the complete contents of the component. For example, the Swing JEditorPane class can display long HTML documents. If you use PrintableComponent to print a JEditorPane, however, it prints only the currently visible text, not the complete HTML document. The ability to print complete documents is a feature that is sorely missing in the current implementation of Swing.

5.2.2. Printing Multipage Documents

As we just discussed, the Printable interface can be used to print multipage documents. However, the PrinterJob has no way of determining in advance how many pages are required. This means that the user cannot request that only a subset of pages be printed, for example. When you know the complete contents of the document to be printed and can break it into pages before printing begins, it is better to use the Pageable interface than the Printable interface.

Pageable defines a getNumberOfPages() method that returns the number of pages to be printed. It also defines two methods that take a page number and return PageFormat and Printable objects for that page. To print a Pageable object, the PrinterJob asks for a PageFormat and a Printable object for each page to be printed and then uses the print() method of each Printable object to print that page.

Example 5-2 shows a class that implements the Pageable and Printable interfaces in order to print a string, file, or stream of text. This is a rudimentary example of text printing. It prints only text files, using a single font, and does not even expand tabs or wrap long lines. The Java 1.2 printing API allows the use of Java 2D graphics through the Graphics2D class. This example does not use the Java 2D version of the drawString() method, however. Although that method allows text to be positioned more precisely using floating-point coordinates, there is a bug in the current implementation that prevents this method from printing correctly.

Example 5-2. PageableText.java

import java.awt.*;
import java.awt.print.*;
import java.io.*;
import java.util.Vector;

public class PageableText implements Pageable, Printable {
  // Constants for font name, size, style and line spacing
  public static String FONTFAMILY = "Monospaced";
  public static int FONTSIZE = 10;
  public static int FONTSTYLE = Font.PLAIN;
  public static float LINESPACEFACTOR = 1.1f;

  PageFormat format;   // The page size, margins, and orientation
  Vector lines;        // The text to be printed, broken into lines
  Font font;           // The font to print with
  int linespacing;     // How much space between lines
  int linesPerPage;    // How many lines fit on a page
  int numPages;        // How many pages required to print all lines
  int baseline = -1;   // The baseline position of the font

  /** Create a PageableText object for a string of text */
  public PageableText(String text, PageFormat format) throws IOException { 
    this(new StringReader(text), format); 
  }

  /** Create a PageableText object for a file of text */
  public PageableText(File file, PageFormat format) throws IOException { 
    this(new FileReader(file), format); 
  }

  /** Create a PageableText object for a stream of text */
  public PageableText(Reader stream, PageFormat format) throws IOException {
    this.format = format;

    // First, read all the text, breaking it into lines.
    // This code ignores tabs and does not wrap long lines.
    BufferedReader in = new BufferedReader(stream);
    lines = new Vector();
    String line;
    while((line = in.readLine()) != null) 
      lines.addElement(line);

    // Create the font we will use, and compute spacing between lines
    font = new Font(FONTFAMILY, FONTSTYLE, FONTSIZE);
    linespacing = (int) (FONTSIZE * LINESPACEFACTOR);

    // Figure out how many lines per page and how many pages
    linesPerPage = (int)Math.floor(format.getImageableHeight()/linespacing);
    numPages = (lines.size()-1)/linesPerPage + 1;
  }

  // These are the methods of the Pageable interface.
  // Note that the getPrintable() method returns this object, which means
  // that this class must also implement the Printable interface.
  public int getNumberOfPages() { return numPages; }
  public PageFormat getPageFormat(int pagenum) { return format; }
  public Printable getPrintable(int pagenum) { return this; }

  /**
   * This is the print() method of the Printable interface.
   * It does most of the printing work.
   */
  public int print(Graphics g, PageFormat format, int pagenum) {
    // Tell the PrinterJob if the page number is not a legal one
    if ((pagenum < 0) | (pagenum >= numPages)) 
      return NO_SUCH_PAGE;

    // First time we're called, figure out the baseline for our font.
    // We couldn't do this earlier because we needed a Graphics object.
    if (baseline == -1) {
      FontMetrics fm = g.getFontMetrics(font);
      baseline = fm.getAscent();
    }

    // Clear the background to white.  This shouldn't be necessary but is
    // required on some systems to work around an implementation bug.
    g.setColor(Color.white);
    g.fillRect((int)format.getImageableX(), (int)format.getImageableY(),
               (int)format.getImageableWidth(), 
               (int)format.getImageableHeight());

    // Set the font and the color we will be drawing with.
    // Note that you cannot assume that black is the default color!
    g.setFont(font);
    g.setColor(Color.black);

    // Figure out which lines of text we will print on this page
    int startLine = pagenum * linesPerPage;
    int endLine = startLine + linesPerPage - 1;
    if (endLine >= lines.size()) 
      endLine = lines.size()-1;

    // Compute the position on the page of the first line
    int x0 = (int) format.getImageableX();
    int y0 = (int) format.getImageableY() + baseline;

    // Loop through the lines, drawing them all to the page
    for(int i=startLine; i <= endLine; i++) {
      // Get the line
      String line = (String)lines.elementAt(i);

      // Draw the line.
      // We use the integer version of drawString(), not the Java 2D
      // version that uses floating-point coordinates. A bug in early
      // Java 1.2 implementations prevents the Java 2D version from working.
      if (line.length() > 0) 
        g.drawString(line, x0, y0);

      // Move down the page for the next line
      y0 += linespacing;  
    }

    // Tell the PrinterJob that we successfully printed the page
    return PAGE_EXISTS;
  }

  /**
   * This is a test program that demonstrates the use of PageableText
   */
  public static void main(String[] args) throws IOException, PrinterException {
    // Get the PrinterJob object that coordinates everything
    PrinterJob job = PrinterJob.getPrinterJob();

    // Get the default page format, then ask the user to customize it
    PageFormat format = job.pageDialog(job.defaultPage());

    // Create our PageableText object, and tell the PrinterJob about it
    job.setPageable(new PageableText(new File(args[0]), format));

    // Ask the user to select a printer, etc., and if not canceled, print!
    if (job.printDialog()) 
      job.print();
  }
}

5.2.3. Efficiency Issues in the Java 1.2 Printing API

Although the Java 1.2 printing API offers important design improvements over the Java 1.1 API, there are serious efficiency problems with Sun's implementation of the 1.2 API in versions of Java up to at least Java 1.2.2. All printers are good at printing text, but not all are equally good at drawing arbitrary graphics. Thus, when a page contains anything but text or very simple graphics, Java 1.2 converts the entire page to a very large image and prints it in graphics mode.

As I mentioned earlier, the current implementation of PrinterJob calls the print() method of a Printable object at least twice. The first call uses a dummy Graphics object whose sole purpose is to determine what kind of graphics the page contains. If the page contains only text, as is the case in Example 5-2, the PrinterJob can print the page efficiently in text mode.

However, if the page contains any other type of graphics, the PrinterJob uses a large, high-resolution image to capture the graphics on the page and then transmits this image to the printer for printing in graphics mode. Because such a high-resolution image is memory intensive, the PrinterJob typically breaks the page up into several smaller bands and calls the print() method several times (using a different clipping region each time). In this way, the PrinterJob is able to spool a large image to the printer without using a large amount of memory (a classic time versus space trade-off).

Unfortunately, the implementation is not well optimized, and printing performance is unacceptable on some systems. Printing even a simple graphic, such as one produced with the PrintableComponent class shown in Example 5-1, can take several minutes and can produce a printer spool file of more than 50 megabytes.

Printing with the Java 1.1 API works better in Java 1.1 than it does in current implementations of Java 1.2. The Java 1.1 API works in Java 1.2, but it suffers the same efficiency problems as the Java 1.2 API. Furthermore, the Java 1.1 API does not perform the first pass to determine what type of graphics a page contains, so even a Java 1.1 program that prints only text is inefficient when run under Java 1.2.



Library Navigation Links

Copyright © 2001 O'Reilly & Associates. All rights reserved.