Printing in Java, Part 2

Print your first page and render complex documents

This month, we will put into practice what we learned in Part 1 of this five-part series on printing in Java. In Part 1, I presented the two printing models: the Printable and the Pageable. I also discussed how to use the Book class to create documents. You will start Part 2 by printing your first page; then you will move on to render more complex documents with Java 2D. In addition, I will explain how to use fonts and all their related classes. And, as promised last month, I will examine the issues concerning printing on different platforms. Note that all examples were compiled and executed using Sun Java 1.3 on both Windows 2000 and Linux.

Print your first page

In Part 1, you learned that the print system in Java has two distinct models: the Printable and the Pageable. Though Printable can print simple documents, it features several limitations, the major one being that all pages must share the same format. The Pageable model, on the other hand, offers much more flexibility. Used in conjunction with the Book class, it can create multipage documents, with each page formatted differently. Listing 1 shows how to print using the Printable model.

Note: All examples, as well as their accompanying source code, can be downloaded from the Resources section below.

Listing 1

Listing 1 will print a half-inch by half-inch grid using the default margin setting, usually one inch for the top, bottom, left, and right margins. Please note that if you try to execute this example with Java 1.2, the margins will not fit; the left margin will be slightly larger than one inch. A bug in Java 1.2's print API causes this glitch.

Now, let's go through the steps required for printing with the Printable model:

  1. Create a PrinterJob object. This object controls the print process by displaying page and print dialogs, and initiating the print action.
  2. Display the proper dialogs, either print or page dialogs.
  3. Create a class that implements the Printable interface's print() method.
  4. Validate the page number to be rendered.
  5. Render your page using the Graphics parameter.
  6. If the page renders, return the PAGE_EXISTS value; if the page does not render, return the NO_SUCH_PAGE value.

Use Pageables and Books to print

As you learned from the previous example, printing with Printable is a straightforward process, but doesn't offer much flexibility when it comes to handling more complex documents. For these types of tasks, the Pageable and the Book classes come in handy. As explained in Part 1 of this series, the Book class allows you to choose whether each Book's page will use a different page painter or the same page painter as other pages. To see the Pageable and the Book at work, let's begin with this example:

Listing 2

This example will print a two-page document using a painter for the cover page and another painter for the document itself. The cover page will print on a portrait-sized sheet, while the second page will be set up to print in a landscape format. Notice that in Listing 2, each painter prints one page; I don't validate the page parameter. To use a painter to print more than one page, consider the next example:

Listing 3

In Listing 3, the code in line 44 adds a third page using the same painter as the first page. In lines 164 through 171, the if statement checks for the right page to render. Since this is a simple example, the code to validate the rendered page is also easy. But, as you discovered from the previous examples, paging a document can be complex. The print framework that I introduce later in this series will allow you to concentrate on the task at hand instead of completing fancy calculations to fit your document on a page.

Now that you know how to render pages using the print API, it might be useful to present a dialog with which the user can choose the destination printer. You also might want to give the user an opportunity to change the page format and margins. The Java Print API offers two dialog windows, the printer dialog and the page dialog.

Use the print and the page setup dialogs

In previous versions of Java, you were required to show the print dialog box only. Starting with Java 1.2, you can display either the print or the page dialog. This enables you to create unattended print jobs, an ability that becomes useful when you want to set a server to print documents. Using dialogs on a desktop-type application promotes user-friendliness. Let's take a look at the print dialog, as shown in Figure 1.

Figure 1. Print dialog

With the print dialog box, the user can select a printer and modify its configuration. The user can also set the number of copies to print and select the page range. Using this dialog, you transfer control of the print job to the user. The dialog box provides the only place where the user can decide to proceed with the print job or cancel it.

Below, you will find a small code excerpt that displays the print dialog:

   if (printJob.printDialog()) {
                                       try {
                                       printJob.print();
                                       } 
                                       catch (Exception PrintException) {
                                       PrintException.printStackTrace();
                                       }
                                       }
                                       
                                      

The printDialog() method, part of the PrinterJob class, shows the print dialog to the user. This method returns a Boolean value. If the user selects the Print button, printDialog() will return true; if the user selects the cancel button, the method returns a false value, and the print job will not proceed. printDialog() only interacts with your application by returning the Boolean. None of the parameters set in this dialog will affect any of PrinterJob's objects.

The second dialog is the page dialog. With this dialog, the user can change the page setup. Figure 2 illustrates an example of a page dialog:

Figure 2. Page dialog

Using this dialog, the user can choose all the parameters of the PageFormat object, including the paper size, the paper source, the orientation of the page, and the margins.

The code excerpt below shows how to invoke the page dialog:

   PageFormat documentPageFormat
                                       = new PageFormat ();
                                       documentPageFormat = printJob.pageDialog (documentPageFormat);
                                       book.append (new Document (), documentPageFormat);    
                                       
                                      

If the user selects the OK button, the pageDialog() method will return a clone of the PageFormat object reflecting the user's setup. If the user selects the Cancel button, then the method returns an unaltered copy of PageFormat.

You have now completed the first step on your path to understanding printing in Java. Let's move on to learn how Java handles text.

Handling text and fonts

Rendering text probably represents the most complex aspect of printing. Long gone are the days of daisy wheel and dot matrix printers, when we only needed to know the number of characters per inch and the width of the page in characters. Full justification of a paragraph was simple, too; we just added spaces between each word until the text aligned with the right margin. To align text vertically, we only sent a CR/LF code to the printer and started printing the next line. Wow, has printing changed! Using today's technology to print two lines of text, one on top of the other, demands much more knowledge, including how to use fonts.

Fonts

If you want to render eye-appealing text, you must first understand the structure of a font. Figure 3 illustrates how fonts are laid out. Characters align along a line called the baseline, which is the vertical point of reference for positioning a font. The ascend is the distance between the baseline and the top of the tallest character in a string. The space between the baseline and a string's lowest glyph is the descend. The leading represents the vertical distance between two characters, and the font height is the ascend plus the leading plus the descend. Finally, we call a string's length the advance. All of these parameters are font dependent, meaning that text rendered using an Arial font will not occupy the same physical space as the same text rendered using a Lucida font.

Figure 3. Font definition

The process of positioning fonts differs from positioning a standard geometrical figure. A rectangle's top left corner sets its origin; the baseline on the Y axis and the first character on the X axis mark a font's origin. See Figure 4 to see the font origin's location.

Figure 4. Font origin

Two styles of fonts are available:

  • Serif fonts feature short lines stemming from and at an angle to the upper and lower ends of a letter. Times New Roman is an example of a serif font.
  • Sans serif fonts do not have any decorative elements. The Arial font is a sans serif font.

Each of these font styles can be either monospaced or proportional. A monospaced font is a font in which all characters have the same width. Old printers, such as daisy wheel or dot matrix printers, used monospaced fonts, as do many text-based interfaces. Each character in a proportional font spans a different width. We were introduced to proportional fonts when laser printers came out. The first commercial computers to use proportional fonts on both screen and printer were Apple's Lisa and Macintosh -- for those that can remember that far back.

Accessing fonts

Since Java was designed to be written once and run anywhere (WORA), Java creators had to produce a default font family that all Java-supported platforms could access. Even more importantly, Java needed to provide a selection of default fonts, whether or not the peer operating system includes them. Java offers eight default fonts: Serif, Sans Serif, Dialog, Dialog Input, Lucida Sans, Lucida Sans Typewriter, Lucida Bright, and Monospace.

To stay on the WORA bandwagon, Java uses logical font names. These names automatically map to an operating system's fonts. The mapping occurs in the JDK directory's jre/lib/font.properties file. In this file, you would discover how a particular operating system font maps to Java's Serif font, for example. A logical font name is a name assigned by Java to an operating system font. If you look at the font.properties file, the logical name is the name that appears on the left side of the equal sign. As an example, Serif is a logical font name that maps to Times New Roman on my computer.

Aside from the logical font name, you can also access fonts by their font face name or font name. The font name is the name given by the font creator, such as Arial, Courier, Times Roman, or Lucida. An example of a font family is Arial; Arial Bold, Arial Italic and Arial Regular are members of the Arial font family.

If your application needs access to more fonts, set the java.awt.fonts parameters of the Java command -- as in java -Djava.awt.fonts=[fonts directory] ... -- to point to a font directory of Type 1 (Postscript) fonts or True Type fonts. This offers a convenient approach for broadening your application's fonts without adding them to the operating system. If your application will run on multiple platforms and you want to make a specific font available to your application, pack the font in your application's jar file.

Related font classes

The Java Graphics API supplies many classes that alleviate the task of manipulating fonts. The definitions of some of the more useful classes follow in the table below:

Classes included in the Java Graphics API

Note: Links to javadocs for these classes are available in

Resources

NameTypeDescription
FontClassThe Font class represents an instance of a font face. Use this class to create a new font based on the available fonts on the target system.
FontMetricsClass (Abstract)This class contains information about a font and the rendering device. FontMetrics features many utilities, one of which obtains a string's width. Note that this class is abstract; to acquire an instance, you must call Graphics.getFontMetrics(). Use FontMetrics to learn a font's basic metric information, such as its descend, ascend, leading, or advance.
FontRenderContextClassThis class provides information about a font related to the rendering device and the rules required to render such a font. Rules define the quality of the rendering.

Please refer to the JDK documentation for a complete explanation of each of these classes. To learn how to use these classes, refer to the code examples in this article.

It's not easy to render paragraphs of text by manipulating raw fonts. Writing an algorithm to fully justify text that has a proportional font is a laborious task. And adding support for international characters only adds to the complexity. That's why we will use the TextLayout and the LineBreakMeasurer classes to render text.

TextLayout to the rescue

TextLayout offers a great deal of functionality in rendering high quality text. This class can render bidirectional text such as Japanese text, where figures align right to left instead of the North American style, which flows left to right. The TextLayout class offers some additional functionalities that we will not use in the course of this series. Features such as text input, caret positioning, and hit testing are not necessary for printing documents, but it's good to know that these features exist.

TextLayout lays out paragraphs, but it doesn't work alone. To arrange text within a specified width, it needs the help of the LineBreakMeasurer class. LineBreakMeasurer will wrap a string of text to fit a predefined width. Since it's a multilingual class, it knows exactly where to break a line of text according to each language's rules. The LineBreakMeasurer class also does not work alone. It needs information from the FontRenderContext class, which, as its main function, returns accurate font metrics. To measure text effectively, FontRenderContext must know the rendering hints for the targeted device and the font type being used.

Before I explain the first example of a TextLayout application, I need to introduce a new class, AttributedString. This class is extremely helpful when you want to embed attribute information within a string. For example, let's say that we have the following sentence:

This is a Bold attribute.

If you printed this string without the AttributedString class, you would use the Graphics.drawString() method, and the code would look something like this:

   Font normalFont = new Font ("serif", Font.PLAIN, 12);
                                       Font boldFont = new Font ("serif", Font.BOLD, 12);
                                       g2.setFont (normalFont);
                                       g2.drawString ("This is a ");
                                       g2.setFont (boldFont);
                                       g2.drawString ("bold ");
                                       g2.setFont (normalFont);
                                       g2.drawString ("attribute", 72, 72);
                                       
                                      

Look how much code we needed just to print a simple line of text. Imagine if we had to render an entire paragraph! We can simplify this example with an AttributedString object. AttributedString is a string class that supports absolute attributes, which require that you specify the start and the end position of attributes.

Here is our previous example using AttributedString:

   AttributedString attributedString = new AttributedString
                                       ("This is
                                       a Bold attribute");
                                       attributedString.addAttribute (TextAttribute.WEIGHT,
                                       TextAttribute.WEIGHT_BOLD, 11, 14);
                                       g2.drawString (attributeString.getIterator (), 72, 72); 
                                       
                                      

In the above example, the word Bold is in boldface type. To apply a bold attribute to the word Bold, use the addAttribute() method. The first parameter represents the attribute key, which identifies the family of the attribute you want to set. For example, a TextAttribute.WEIGHT's attribute key indicates that the set attribute will be of type weight.

The second parameter is the attribute itself. If we set the first parameter to TextAttribute.WEIGHT, then the available attributes are WEIGHT_DEMIBOLD, WEIGHT_DEMILIGHT, WEIGHT_EXTRA_LIGHT, WEIGHT_EXTRABOLD, WEIGHT_HEAVY, WEIGHT_LIGHT, WEIGHT_MEDIUM, WEIGHT_REGULAR, WEIGHT_SEMIBOLD, and WEIGHT_ULTRABOLD.

The third parameter marks the position at which the program should begin applying the attribute, and the fourth and last parameter indicates where it should stop. You can also apply an attribute to an entire string by using the same method without supplying the start and end parameters. Figure 5 illustrates the structure of the text example in an AttributedString.

Figure 5. Structure of an AttributedString

Try the TextLayout

We have now acquired an understanding of all the classes that we need to work with the TextLayout class. Example 4 from this article's source code (available in Resources) implements TextLayout to render a paragraph; Listing 4 provides an excerpt. The width of the paragraph has been set to 7.5 inches. The TextLayout code is located between lines 162 and 235.

Listing 4

To summarize Listing 4: We created an AttributedString. The addAttribute() method sets the font. We then create a LineBreakMeasurer with two parameters passed to it, a character iterator from the AttributedString, and a FontRenderContext to supply the necessary font metrics. In return, the LineBreakMeasurer will provide a TextLayout object for each wrapped line. The loop completes some basic math to position each line of text. Keep in mind that fonts are not positioned from their top left corner, like rectangles. Instead, the y=0 is located at the baseline of the font. Also, if you want to properly position one line of text on top of another, you must add the descend and the leading together. Refer back to Figure 1 for a graphical representation of the structure of a font.

The next example will implement full justification. We need to add a few lines of code to the previous example. Here is an excerpt of Example 5:

Listing 5

Listing 5's most interesting code section stretches from line 219 to line 259. Although the TextLayout class features a method that fully justifies a line of text, the LineBreakMeasurer includes no functionality for detecting the paragraph's last line of text. Since a fully justified paragraph justifies all lines but the last, we need a way to detect the paragraph's last line and print it without justification.

In order to detect the last line, we must first wrap all lines and store them in a Vector. Then a for loop will scan each line to check if it's the last line of the paragraph. If it's not, the getJustifiedLayout() method justifies it. If it is indeed the last line, it is rendered as is.

Printing images

Printing an image is as straightforward as loading it in an Image object and invoking the Graphics2D.drawImage() method. The following example illustrates how to print the NASA Space Station image; see lines 165 and 198 for the actual code that prints the image.

Listing 6

The process of printing an image is divided into three easy steps.

  1. Create a URL that will point to the image you want to print.
  2. Load the image using a MediaTracker class. With this class, you can load GIF, JPEG, and PNG files. If you want to load other image types, use the Advance Imaging API. Check out JavaWorld's Java Tip 43 for more information on loading bitmaps (direct link available in Resources).
  3. Draw the image using the drawImage() method of the Graphics2D class. drawImage() needs six parameters. The first is the Image object to print; the second and third parameters are coordinates for the image's top left position, and the fourth and fifth parameters specify the image's width and height. The last parameter is for the ImageObserver. For the purpose of this example, the Document extends the Component class. All Component classes implement the ImageObserver interface.

By following these three easy steps, you can easily render images on paper. If you need to support more formats, you can always download the Advanced Imaging API from the JavaSoft Website.

Printing on multiple platforms

Although Java has been qualified as a WORA language, it must rely on some services provided by the underlying operating system. In order to validate the issues of printing on multiple platforms, I set up a Linux machine with SuSE 6.4 and Sun Java 1.3. To make the comparison fair between Windows 2000 and Linux, I connected the same HP 5L printer to both computers. My main concern was not the output quality, but how Java handles the differences between Windows, which provides standard print and page setup dialogs, and Linux, which doesn't support standard dialogs.

The first problem I encountered was configuring the printer. SuSE Linux uses the APS print filter, and I could not configure my HP 5L to print properly at 600 by 600 dpi. My printer only worked when set as an HP LaserJet 4 with a resolution of 300 dpi instead of the 600 dpi it could produce if used with the proper driver.

I ran my code on Linux without any changes made from the Windows version, and, despite the resolution difference, it printed with excellent quality and superb performance. The display of the print and page dialogs surprised me. They appeared completely different from their Windows counterparts. Figures 6 and 7 show these dialogs:

Figure 6. Page dialog in Linux
Figure 7. Print dialog in Linux

The drastic differences in dialogs will deprive your application of some functionality. For example, under Linux, you cannot use the print dialog to select a page range, while you can under its Windows counterpart. In addition, Linux's page dialog has no support to set the margins. If you're writing portable applications, you will have to deal with these major disparities and test your applications on all targeted platforms. As I don't yet have access to a Mac running OS X, I cannot offer an example with this platform. I will attempt to access this new OS soon and share my results in a future article.

Conclusion

Part 2 introduced you to the pitfall of the Java Print API: it only provides a graphic canvas to draw on. As the API lacks such concepts as paragraphs, images, running headers/footers, and tables, you must make primitive calls to it to add functionality. I also touched on the issue of printing on multiple platforms and described the differences between printing under Linux and printing under Windows. In Part 3, I will explain the design of the print framework that we will build in the remaining three articles. Until then, happy printing!

Jean-Pierre Dubé is an independent Java consultant. He founded Infocom in 1988. Since then, Infocom has developed custom applications in fields including manufacturing, document management, and large-scale electrical power line management. Jean-Pierre has extensive programming experience in C, Visual Basic, and Java; the latter is now the primary language for all new projects. He dedicates this series to his mother, who passed away while he was writing this article.

Learn more about this topic

Join the discussion
Be the first to comment on this article. Our Commenting Policies
See more