Printing in Java, Part 4

Code the print framework

Many of you have waited a long time for this month's article. In the last three articles, you have learned about the strengths and weaknesses of the Java Print API. In Part 1, you learned about the different models that the Java API uses to produce printed output. Part 2 presented code examples of those models. Part 3 introduced you to the print framework. Now that everything is clear in your head, you can start implementing that framework.

The project's structure

Before you begin coding, I would like to explain how I organized the project, what tools I used for its development, how I tested it, and where to find the appropriate files.

I used an integrated development environment (IDE) called Java Development Environment (JDE), which is a nice add-on to Emacs. Written by Paul Kinnucan, JDE incorporates great features, like an integrated debugger, code completion, and comment generation.

I also used Ant 1.1 to build this project. Ant is a powerful scripting tool, similar to the make utility. But unlike make, Ant easily defines rules by using an XML file. Because it's written in Java, Ant is portable. If you are unfamiliar with the Ant tool, read Michael Cymerman's excellent article, "Automate your build process using Java and Ant" (JavaWorld, October 20, 2000).

Listing 1 provides the content of my Ant file, build.xml:

Listing 1: build.xml

If you use my Ant file, don't forget to change the base directory at the top of the file to your base directory. Refer to Figure 1 for more details on this project's directory structure.

Every Java programmer should complete some form of unit testing. Unit testing not only tests your code, it can also provide excellent examples on how to use your code. In this series, all the code examples come from test fixtures.

To test my code, I used JUnit 3.2, a high-quality testing framework. I specifically like JUnit's ability to test code as either a black box, as the code-user will see it, or a white box, from the inside. The two testing modes differ based on the location of your test fixtures. If you place your test fixtures in the same package as your test program, then you perform white-box testing; you can do black-box testing by placing your test fixtures in another package. In this series, the test fixtures will feature black-box testing.

For the framework's file organization, see Figure 1. In the root directory named PrintFrameWork, you will find all the subdirectory files related to this project.

Figure 1. Project file structure

Table 1 defines each directory:

buildContains the print framework library (printframework.jar)
classesContains output directory for the Java compiler
testContains source code for unit testing
imagesContains images needed by the print framework library -- those images are copied by ANT to the classes subdirectory when a rebuild is complete
srcContains source files for the print framework library
Table 1. Project file structure

Now that you know the files' location, you can begin coding. Start by implementing all the measurement classes. If you need to review the framework's design, refer to UML Diagram 1.

Implement the measurement system classes

You must implement the measurement classes because every other class in the framework relies on them.

The PFUnit class forms the heart of the measurement system; only the getPoints() and setPoints() methods are left abstract. Place your code that converts the measurement unit to points in getPoints(), and the code that converts from points to the measurement unit, in the setPoints() method.

All the basic math operations have been implemented in PFUnit. The math methods support either a double value or a PFUnit class as their input. When you pass a double value in parameters, the method assumes that the value is in the measurement unit represented by the class. For example, a double value passed to one of the math methods in the PFInchUnit class is assumed to be in inches. The implementation of PFUnit is in Listing 2.

Listing 2: PFUnit

The next code segment shows how easily you can implement your own measurement system. This segment implements the PFInchUnit class.

1|package com.infocom.print; 2| 3|/** 4| * Class: PFInchUnit <p> 5| * 6| * @author Jean-Pierre Dube <> 7| * @version 1.0 8| * @since 1.0 9| * @see PFUnit 10| */ 11| 12|public class PFInchUnit extends PFUnit { 13| 14| //--- Private constants declarations 15| private final static int POINTS_PER_INCH = 72; 16| 17| 18| /** 19| * Constructor: PFInchUnit <p> 20| * 21| */ 22| public PFInchUnit () { 23| 24| } 25| 26| 27| /** 28| * Constructor: PFInchUnit <p> 29| * 30| * @param parValue a value of type double 31| */ 32| public PFInchUnit (double parValue) { 33| 34| super (parValue); 35| 36| } 37| 38| 39| /** 40| * Method: getPoints <p> 41| * 42| * Return the result of the conversion from 43| * inches to points. 44| * 45| * @return a value of type double 46| */ 47| public double getPoints () { 48| 49| return (getUnits () * POINTS_PER_INCH); 50| 51| } 52| 53| 54| /** 55| * Method: setPoints

56| * 57| * @param parPoints a value of type double 58| */ 59| public void setPoints (double parPoints) { 60| 61| setUnits (parPoints / POINTS_PER_INCH); 62| 63| } 64|}// PFInchUnit

Line 49 in the code above returns the units (inches) converted to points. There are 72 points per inch, so converting inches to points requires simply multiplying the inch value by 72. The PFUnit library features two other measurement unit classes: PFCmUnit and PFPointUnit.

Next, implement the PFPoint, PFSize, and PFRectangle classes. Use those classes to represent geometrical coordinates with the aforementioned measurement system.

The PFPoint class has more functionality than the point classes included in the Java Print API. For example, PFPoint can add and subtract another PFPoint. Since the math operations are repetitive, I decided to include the code in the class itself; it also improves our object-oriented design.

To represent a size in the framework, use the PFSize class. Its only noteworthy feature is the scale() method, which allows you to scale the size by two factors: one for the width and one for the height. scale() is useful for zooming pages.

You can use the PFRectangle class to represent a rectangular area. PFRectangle offers two ways to set a rectangle's location and size: set the x, y, width, and height separately, or use a PFPoint class to set the location and a PFSize class to set the size. No matter how you set the coordinates and size, PFRectangle uses a PFPoint and a PFSize to keep track of its location and size.

This concludes our discussion on the measurement system implemented in the print framework. PFUnit provides the foundation; by extending PFUnit, you can create custom measurement systems like PFInchUnit and PFCmUnit. In addition, the PFPoint, PFSize, and the PFRectangle represent geometrical coordinates. Next I will explain the PFDocument class.

The PFDocument class

At the head of the print framework, you will find PFDocument -- the master class where every print or export operation instantiates. Let's examine PFDocument from the inside:

Listing 3: PFDocument

The class first acts as a page container. To store the pages, a Vector class is used. (See line 31.)

Next, two flags, located in lines 38 and 39 in Listing 3, will tell the print() method whether to show the page dialog and/or the print dialog.

Then come the headers instances (lines 42-45), which hold the header and footer for the current page.

I created several methods between lines 59 and 139 in Listing 3 that add and remove pages. The addPage() method (line 67) lets you append a page to the document. The additional addPage() method (line 84) allows you to insert a page after the page number passed in the parameter. Two removePage() methods complement the two addPage() methods. The first removePage() (line 100) removes a page using its page number; the other removePage() (line 115) removes a page using the PFPage source object.

Two methods -- showPrintDialog() and showPageDialog() -- control the page dialog and print dialogs. The methods, located between lines 188 and 218 in Listing 3, do not show the dialogs; they only set the appropriate flags. The print() method handles the display of the dialogs.

The print() method (lines 234-296 in Listing 3) links the print framework with the Java Print API. Let's examine that method.

First, a PFPage object is created; it will be used in the for loop to store each page that is printed. Then a PageFormat object is created; it will hold the page format settings from the page setup dialog. Those settings will only be used if the showPageDialog flag has been set.

Next, the page range passed in parameters is validated. If the page range falls outside the allowable range, the values adjust. A printerJob object is then created. The setJobName() method applies the document name to the PrinterJob object. Then, at line 262, the showPageSetupFlag is checked. If true, the page setup dialog displays.

You then must create the Book object, which will hold all the pages contained in the document. Enter in a loop that will add all the pages from the pageCollection to the Book object. In the loop, another if statement validates what PageFormat you should use. If the PFPageSetupDialog displays, every document page will set to the page setup that the user selected. Otherwise, the page format associated with the page is used. Once the loop finishes processing every document page, it adds the book object to the printerJob object. (See line 281 in Listing 3.)

Finally, enter the try/catch block, where you ask printerJob to print the document. Before the print process starts, validate the showPrintSetupDialog flag. If set, the standard Java Print Dialog displays. With that dialog, the user can either proceed with the print operation or cancel it; the if statement at line 287 in Listing 3 validates showPrintSetupDialog. If the print dialog returns a true value, proceed with the print process; otherwise, abort it.

I did not implement the next two methods, printPDF() and printHTML(), because the subject is quite complex and stretches beyond the scope of this series. I leave their implementation to you, but I will offer some suggestions. First, the PFDocument generates the HTML file's header portion. Then, a for loop similar to the one in the print() method obtains the HTML code for each document page. Finally, another for loop obtains each object's HTML code for each page. A close look at UML Diagram 1 reveals that each object in the hierarchy must provide the appropriate code, either HTML or PDF.

Our discussion on the PFDocument class will end with the printPreview() method. When called, printPreview() will show the document in a preview window (PFPrintPreview). I will cover the implementation of printPreview() in Part 5.

The PFPage class

The PFPage class represents a page entity. A document is a collection of pages; a page is a collection of print objects. In the print framework, you must create an instance of PFPage for each page in your document. Let's examine its construction:

Listing 4: PFPage

PFPage uses a Vector object to store print objects and supports several methods of adding and removing print objects. You will find the implementation of those methods between lines 130 and 156 in Listing 4.

To store page parameters like margins, page size, and page orientation, I use a PFPageFormat class, which offers various functionalities. It allows the PFPageSetupDialog to obtain and return the page parameters in one fell swoop. And if the user selects a different page format, the PFDocument print() method can assign the new parameters using a single line of code. Imagine how much code you would need to set the top, left, right, bottom, gutter, page size, and orientation for each page! And finally, PFPageFormat places items where they belong -- a good design decision.

The only way to set a page's format is to obtain the PFPageFormat from the page, adjust the parameters, and set the PFPageFormat using the setPageFormat() method. The PFPageFormat object returned by getPageFormat() is a clone of the original. You will find a description of the PFPageFormat class in Listing 3.

The most interesting part of PFPage may be the print() method located between lines 79 and 131 in Listing 4. The Graphics object converts to a Graphics2D object. This conversion must take place at the start of the method because from then on, all the PFPrintObject classes render themselves with a Graphics2D object.

The next section of code renders a header and footer of your choice. The getHeader()/getFooter() methods will make the final decision based on the following rule: The page header/footer is used if it is not null, otherwise the methods ask PFDocument for a non-null header/footer value. If the answer is positive, the document header/footer is used. Finally, the header and footer are positioned on the page. See Figure 2 for the exact positions.

1 2 Page 1
Page 1 of 2