SDK 1.2 Printing API: A Tutorial
February
08, 2000
Draft Version 0.7
Table of Contents
Introduction
The Java
TM
2 Standard Development Kit, version 1.2 introduced a new printing application
programming interface (API). This application interface, referred to as
the SDK 1.2 Printing API or Printing API in this document, is independent
of the Java 2DTM API,
although it was introduced along with the powerful Java 2D graphics library
and designed to image the complete set of Java 2D graphics.
This document introduces the Java 2
SDK, version 1.2 Printing API, discusses possible future capabilities of
the API, and provides examples of its use.
Requirements and Restrictions
Print All of Java 2D
The breadth of Java2D graphics that can
be printed through the SDK 1.2 Printing API exceeds the capabilities of
most graphics subsystems, such as Windows GDI and Macintosh Quickdraw.
Java 2D also exceeds the power of existing printer control languages, such
as PCL 5 and PostScript. Nonetheless, one of the requirements for the new
Printing API was that it print the full set of Java 2D grqaphics, even
though the host and printer capabilities are overmatched by Java2D. This
requirement meant that the Printing API, in some situations, would need
to rasterize Java 2D graphics on the host computer.
No Spool Format Available
To rasterize print jobs on a host computer,
a print subsystem typically spools graphics primitives to disk. As each
band of the raster needs to be sent to the printer, the spool file is replayed
in order to render the band. Neither the AWT nor the Java 2D graphics APIs
provide a spool format for their primitives. This lack of an existing spool
format combined with the tight development schedule of SDK 1.2 meant that
a non-spooling solution be implemented to support host rasterized print
jobs. The solution implemented in the SDK 1.2 Printing API has the print
subsystem call back into the application to rasterize a band
.
What the Printing API Is Not
A complete printing system consists of
several layers, extending from the printing application down to the printer,
as illustrated in Figure 1. The application interacts with the printing
system for two reasons: to target a printer and to generate printed pages.
Figure 1: A Print Subsystem
The act of targeting a printer is termed
printer discovery. In its complete form, printer discovery allows
an application to obtain a list of available printers and their capabilities
and to choose one of these printers for subsequent operations.
Printer Imaging consists of formatting
pages and drawing the contents of each page. The conversion of printing
operations to a format supported by the targeted printer, in a complete
printing system, is the job of a printer driver. The printer driver layer
should support plug-in printer drivers.
The SDK 1.2 Printing API primarily
supplies the Printer Imaging portion of the print subsystem. Through the
Printing API, applications can format pages and draw their contents. Printer
discovery is not supported by the SDK 1.2 Printing API, as shown in Figure
2. An application can obtain information about the current printer and
print to it by using the Printing API. The printing dialog supplied by
the Printing API also allows a user to change the current printer, but
the application can not do this programmatically.
Figure 2: The SDK 1.2 Printing API
Does Not Support Printer Discovery
The current implementation of the SDK
1.2 Printing API does not support a printer driver layer (see Figure 2).
Each SDK 1.2 port re-implements the imaging to the printer translation
layer.
Implementation Notes
<To come>
SDK 1.2
The primary goal of the printing support
in SDK 1.2 was to be able to render all Java 2D graphics. To meet this
goal within the SDK schedule, the initial implementation focused upon the
creation of a raster print path and the creation of the new printing API
itself. In this path Java 2D is used to render all the graphics on the
host computer and then the raster is sent to the printer. Both the Windows
and Solaris reference implementations include white space skipping in order
to limit the amount of data sent to the printer. Even with this small optimization,
the amount of generated data can be huge. At the end of the SDK 1.2 development
cycle an optimized shape-printing path was added to avoid the rasterization
path for certain pages in a print job. In the shape-print path, graphics
on the page are converted into shapes and then filled. This generates much
less data than the raster path, but a large amount of data can still be
generated for complex shapes, such as a page of text. Overall, however,
the shape path is faster than the raster path. If a given page only draws
with solid, opaque colors, such as those created with java.awt.Color,
and does not draw images then the shape-printing path will be used to render
the page.
The PrinterJob
Class
With the absence of printer discovery
in the SDK 1.2 Printing API, there is no Printer
class to represent instances of available printers; there is only the current
printer and the current printer job. These concepts are both represented
by the PrinterJob
class.
An application written for the JavaTM
platform obtains an instance of a PrinterJob
by invoking the static PrinterJob
methodgetPrinterJob().
This method returns an instance of the platform-dependent PrinterJob
subclass. For example in SDK 1.2, getPrinterJob()
returns an instance of WPrinterJob
and on Solaris a PSPrinterJob
instance is returned. The actual subclass represented by the returned instance
is not meaningful to the printing application because the application only
invokes public PrinterJob
methods. The PrinterJob
is obtained as follows:
PrinterJob printerJob = PrinterJob.getPrinterJob();
The Book
Class
To print a document, the application written
for the Java platform should build a representation of the document. Instead
of explicitly building this representation, the application can use the
setPrintable()
method that PrinterJob
supports, but the better method to use is setPageable()
because it is a purer and more powerful printing path.
The Pageable
interface allows the print subsystem to query the application for the number
of pages in the document to be printed and to obtain, for each page, the
PageFormat, the
painter, and the Printable. The Pageable
interface enables the print subsystems to print pages in any order and
to draw each page multiple times to implement banded raster printing, separations,
and other special features.
The SDK 1.2 Printing API supplies a
concrete implementation of the Pageable
interface in the Book
class. This Book
class is suitable for most applications, but an application can directly
implement the Pageable
interface to better integrate printing with its internal document structure.
Once a new Book
instance is created, the application appends to the Book
a pair of objects to represent each page of the document. The pair of objects
is made up of an instance of PageFormat
and an instance of an object that implements the Printable
interface. The PageFormat
instance describes the page dimensions and orientation. The Printable
interface is called to paint the contents of the page.
The creation of a one-page Book
looks as follows where PrintBlank
is a class that implements the Printable
interface:
Book book = new Book();
book.append(new PrintBlank(), new PageFormat());
After the instance of Book
is created, it can be added to the PrinterJob:
printerJob.setPageable(book);
The Print Dialog
In the Printing API, the application does
not need to display a print dialog to the user. Most user-oriented applications,
as opposed to server applications, will want to display a print dialog.
The contents of the print dialog is platform-dependent, but it usually
provides the user with page range controls, controls over the number of
copies, and perhaps the ability to switch printers. The PrinterJob
object's printDialog()
method displays the dialog. If the user cancels the dialog then the method
returns false and the application should abort the printing process. If
the printDialog()
returns true, the user has confirmed that printing should occur, as in
Listing 1.
Listing 1: printDialog()
confirms that printing should occur
boolean doPrint = printerJob.printDialog();
if (doPrint) {
// The user confirmed that printing should proceed.
}
Printing
Because the printing loop is controlled
by the printing subsystem, once a PrinterJob
has been instanced and a Pageable
implementation has been added to it, the actual printing process is simple
from the application's point of view. To start printing, the application
invokes the PrinterJob
object's print
method:
printerJob.print();
The printing subsystem then invokes the
application's page painters, the Printables,
as needed. Each Printable
can be invoked multiple times in any order.
A Simple Example: PrintBlank
The PrintBlank
class, shown in Listing 2, puts the Printing API components all together
to provide a simple example of using the SDK 1.2 printing API. This application
presents the user with a print dialog. If the user hits the print
button, one blank page is printed.
Listing 2: Putting Together the
Printing API Components: PrintBlank
import java.awt.*;
import java.awt.print.*;
/**
* This example shows how to use the PrinterJob
* and Book classes.
*/
public class PrintBlank implements Printable {
/**
* Print a single, blank page.
*/
static public void main(String args[]) {
/* Get the representation of the current printer and
* the current print job.
*/
PrinterJob printerJob = PrinterJob.getPrinterJob();
/* Build a book containing pairs of page painters (Printables)
* and PageFormats. This example has a single page.
*/
Book book = new Book();
book.append(new PrintBlank(), new PageFormat());
/* Set the object to be printed (the Book) into the PrinterJob.
* Doing this before bringing up the print dialog allows the
* print dialog to correctly display the page range to be printed
* and to dissallow any print settings not appropriate for the
* pages to be printed.
*/
printerJob.setPageable(book); |
/* Show the print dialog to the user. This is an optional step
* and need not be done if the application wants to perform
* 'quiet' printing. If the user cancels the print dialog then false
* is returned. If true is returned we go ahead and print.
*/
boolean doPrint = printerJob.printDialog();
if (doPrint) { |
try {
printerJob.print();
} catch (PrinterException exception) {
System.err.println("Printing error: " + exception);
}
}
}
/**
* Print a blank page.
*/
public int print(Graphics g, PageFormat format, int pageIndex) {
/* Do the page drawing here. This example does not do any
* drawing and therefore blank pages are generated:
* "This Page Intentionally Left Blank"
*/
return Printable.PAGE_EXISTS;
}
}
The Printable
Interface
When building a Book
the application must supply one or more Printable instances. These Printable
instances are responsible for drawing the contents of each page. A Printable's
print method is
invoked by the printing subsystem as needed to render some or all of a
page. Listing 3 features PrintText,
a sample application that uses its print
method to draw a page of text.
Listing 3: Using the print
method: PrintText
import java.awt.*;
import java.awt.font.*;
import java.awt.geom.*;
import java.awt.print.*;
import java.text.*;
/**
* The PrintText application expands on the
* PrintExample application in that it images
* text on to the single page printed.
*/
public class PrintText implements Printable {
/**
* The text to be printed.
*/
private static final String mText =
"Four score and seven years ago our fathers brought forth on this "
+ "continent a new nation, conceived in liberty and dedicated to the "
+ "proposition that all men are created equal. Now we are engaged in "
+ "a great civil war, testing whether that nation or any nation so "
+ "conceived and so dedicated can long endure. We are met on a great "
+ "battlefield of that war. We have come to dedicate a portion of "
+ "that field as a final resting-place for those who here gave their "
+ "lives that that nation might live. It is altogether fitting and "
+ "proper that we should do this. But in a larger sense, we cannot "
+ "dedicate, we cannot consecrate, we cannot hallow this ground."
+ "The brave men, living and dead who struggled here have consecrated "
+ "it far above our poor power to add or detract. The world will "
+ "little note nor long remember what we say here, but it can never "
+ "forget what they did here. It is for us the living rather to be "
+ "dedicated here to the unfinished work which they who fought here "
+ "have thus far so nobly advanced. It is rather for us to be here "
+ "dedicated to the great task remaining before us--that from these "
+ "honored dead we take increased devotion to that cause for which "
+ "they gave the last full measure of devotion--that we here highly "
+ "resolve that these dead shall not have died in vain, that this "
+ "nation under God shall have a new birth of freedom, and that "
+ "government of the people, by the people, for the people shall "
+ "not perish from the earth."; |
/**
* Our text in a form for which we can obtain a
* AttributedCharacterIterator.
*/
private static final AttributedString mStyledText = new AttributedString(mText);
/**
* Print a single page containing some sample text.
*/
static public void main(String args[]) {
/* Get the representation of the current printer and
* the current print job.
*/
PrinterJob printerJob = PrinterJob.getPrinterJob();
/* Build a book containing pairs of page painters (Printables)
* and PageFormats. This example has a single page containing
* text.
*/
Book book = new Book();
book.append(new PrintText(), new PageFormat());
/* Set the object to be printed (the Book) into the PrinterJob.
* Doing this before bringing up the print dialog allows the
* print dialog to correctly display the page range to be printed
* and to dissallow any print settings not appropriate for the
* pages to be printed.
*/
printerJob.setPageable(book); |
/* Show the print dialog to the user. This is an optional step
* and need not be done if the application wants to perform
* 'quiet' printing. If the user cancels the print dialog then false
* is returned. If true is returned we go ahead and print.
*/
boolean doPrint = printerJob.printDialog();
if (doPrint) { |
try {
printerJob.print();
} catch (PrinterException exception) {
System.err.println("Printing error: " + exception);
}
}
}
/**
* Print a page of text.
*/
public int print(Graphics g, PageFormat format, int pageIndex) {
/* We'll assume that Jav2D is available.
*/
Graphics2D g2d = (Graphics2D) g;
/* Move the origin from the corner of the Paper to the corner
* of the imageable area.
*/
g2d.translate(format.getImageableX(), format.getImageableY());
/* Set the text color.
*/
g2d.setPaint(Color.black);
/* Use a LineBreakMeasurer instance to break our text into
* lines that fit the imageable area of the page.
*/
Point2D.Float pen = new Point2D.Float();
AttributedCharacterIterator charIterator = mStyledText.getIterator();
LineBreakMeasurer measurer = new LineBreakMeasurer(charIterator, g2d.getFontRenderContext());
float wrappingWidth = (float) format.getImageableWidth(); |
while (measurer.getPosition() < charIterator.getEndIndex()) {
TextLayout layout = measurer.nextLayout(wrappingWidth);
pen.y += layout.getAscent();
float dx = layout.isLeftToRight()? 0 : (wrappingWidth - layout.getAdvance());
layout.draw(g2d, pen.x + dx, pen.y);
pen.y += layout.getDescent() + layout.getLeading();
}
return Printable.PAGE_EXISTS;
}
The print
method for a given page may be invoked multiple times so that the print
subsystem can gather metrics about the page or rasterize bands of the page.
On each invocation, the clip of the Graphics
instance parameter is set to the area of the page that should be imaged.
To increase performance, an application should only draw graphics that
intersect the clip. The example above does not perform this optimization.
The Printable.print
method is invoked with three parameters. The first parameter is an instance
of a Graphics subclass.
In a Java 2D system, this parameter will be an instance of a Graphics2D
subclass. If an application uses Java 2D, then it should try to cast this
first print parameter to a Graphics2D
instance. If the cast succeeds, both Graphics2D
and Graphics methods
may be invoked. If the cast fails, only Graphics
methods may be used to draw the page.
The Graphics
parameter passed to the print method will also implement the PrinterGraphics
interface. While usually not needed, this interface allows the print method
to obtain the PrinterJob
that controls the print job. The PrinterGraphics
interface is discussed later in this document, and an example of its use
is given.
The second print parameter is the PageFormat
instance for the page to be drawn. The page painter can use this instance
to obtain the paper size, imageable area, and orientation. Applications
can supply a PageFormat
subclass providing other information to the print method.
Lastly, the print()
method is passed the index of the page to be drawn. This index starts at
0 for the first page in the Book
and each page is numbered sequentially from the first. The page index passed
to the print()
method is based upon a page's position in a Book,
not the page's position in the job. For example, in a five-page Book,
the user may, using the print dialog, request that only page four be printed.
In this case, the print()
method will be passed a zero-based page index of three. Note that the page
index is three even though the page being printed is the first and only
page in the job.
The Page Format Dialog
In addition to the print dialog, a
PrinterJob
provides a page setup dialog. The page setup dialog allows the user to
choose a paper size and orientation. In the SDK 1.2 release, the page setup
dialog is not displayed by the Solaris reference release. The method that
displays the page setup dialog is pageDialog().
The pageDialog()method
takes a PageFormat
instance as its parameter. The PageFormat
is used to initialize the page setup dialog presented to the user. If the
user confirms the dialog then a new PageFormat
instance cloned from the PageFormat
parameter is returned from the method. If the user cancels the page format
dialog then the PageFormat
instance parameter is returned unaltered. Listing 4 modifies the PrintText
main() method (refer
to Listing 3) so that the application displays a page format dialog:
Listing 4: Initializing
the Page Setup Dialog Using the PrintText main()
Method
/**
* Print a single page containing some sample text.
*/
static public void main(String args[]) {
/* Get the representation of the current printer and
* the current print job.
*/
PrinterJob printerJob = PrinterJob.getPrinterJob();
/* Let the user choose a paper size and orientation.
*/
PageFormat format = new PageFormat();
format = printerJob.pageDialog(format);
/* Build a book containing pairs of page painters (Printables)
* and PageFormats. This example has a single page containing
* text.
*/
Book book = new Book();
book.append(new PageSetupText(), format);
/* Set the object to be printed (the Book) into the PrinterJob.
* Doing this before bringing up the print dialog allows the
* print dialog to correctly display the page range to be printed
* and to dissallow any print settings not appropriate for the
* pages to be printed.
*/
printerJob.setPageable(book);
/* Show the print dialog to the user. This is an optional step
* and need not be done if the application wants to perform
* 'quiet' printing. If the user cancels the print dialog then false
* is returned. If true is returned we go ahead and print.
*/
boolean doPrint = printerJob.printDialog();
if (doPrint) { |
try {
printerJob.print();
} catch (PrinterException exception) {
System.err.println("Printing error: " + exception);
}
}
}
The PageFormat
Class
A PageFormat
instance describes the size of the paper and the imageable area of the
page into which the application will draw. The PageFormat
maps the printer's paper size and hardware margins through a series of
transformations representing the drawing orientation and any other features.
The application is presented with the transformed paper dimensions and
with the transformed imageable area.
To get the width of the paper in the
drawing orientation, the application calls the getWidth()
method of PageFormat.
The getHeight()
method returns the transformed height. For example, let us assume that
the underlying paper size is 8.5 inches wide by 11 inches tall (see Figure
3). If the PageFormat
is portrait orientation, then getWidth()returns
612 points (8.5 inches times 72 points per inch). Similarly getHeight()returns
792 points. If, however, the PageFormat
is landscape orientation, as shown in Figure 4, getWidth()returns
792 points and getHeight()returns
612 points.
Figure 3: Portrait PageFormat
Dimensions and Coordinates
|
Figure 4: Landscape PageFormat
Dimensions and Coordinates |
The application obtains the imageable
area of the PageFormat
using the four methods: getImageableX()
, getImageableY(),
getImageableWidth(),
and getImageableHeght().
These values, like the paper dimensions, are provided in 1/72 of an inch.
The top-left corner of the paper is the origin for the coordinate system.
The imageable area coordinates have been transformed like the paper dimensions
to take into account the orientation and other features of the PageFormat.
Applications can subclass PageFormat
in order to alter the paper and imageable area dimensions seen by the application.
The Printing API does not explicitly allow a PageFormat
to perform any drawing, an oversight, but this is remedied by a convention
in Printable's print()
method. Specifically, the application's Printable
can check to see if the PageFormat
instance it is passed implements Printable.
If the PageFormat
does implement Printable
then the PageFormat's print()
method can be invoked. FooterFormat,
shown in Listing 5, demonstrates this convention, using it to have the
PageFormat print
page footers.
Listing 5: Invoking PageFormat's
print()
method
implementing Printable:
FooterFormat
import java.awt.*;
import java.awt.font.*;
import java.awt.print.*;
import java.text.*;
import java.util.*;
public class FooterFormat extends PageFormat implements Printable {
/**
* The font we use for the footer.
*/
private static final Font mFooterFont = new Font("Serif", Font.ITALIC, 10);
/**
* The amount of space at the bottom of the imageable area that we
* reserve for the footer.
*/
private static final float mFooterHeight = (float) (0.25 * 72);
/**
* The format for the date string shown in the footer.
*/
private static final DateFormat mDateFormat = new SimpleDateFormat();
/**
* A formatted string describing when this instance was
* created.
*/
private String mDateStr = mDateFormat.format(new Date()).toString();
/**
* Tell the caller that the imageable area of the paper is shorter
* than it actually is. We use the extra room at the bottom of the
* page for our footer text.
*/
public double getImageableHeight() { |
double imageableHeight = super.getImageableHeight() - mFooterHeight;
if (imageableHeight < 0) imageableHeight = 0;
return imageableHeight;
}
/**
* Draws the footer text which has the following format:
* <date>
*/
public int print(Graphics g, PageFormat format, int pageIndex) {
/* Make a copy of the passed in Graphics instance so
* that we do not upset the caller's current Graphics
* settings such as the current color and font.
*/
Graphics2D g2d = (Graphics2D) g.create();
g2d.setPaint(Color.black);
g2d.setFont(mFooterFont);
LineMetrics metrics = mFooterFont.getLineMetrics(mDateStr, g2d.getFontRenderContext());
/* We will draw the footer at the bottom of the imageable
* area. We subtract off the font's descent so that the bottoms
* of descenders remain visable.
*/
float y = (float) (super.getImageableY() + super.getImageableHeight()- metrics.getDescent() - metrics.getLeading());
// Cast to an int because of printing bug in drawString(String, float, float)!
g2d.drawString(mDateStr, (int) super.getImageableX(), (int)y);
g2d.dispose();
return Printable.PAGE_EXISTS; |
}
}
Listing 6 features TextWithFooter,
an application that uses the FooterFormat
class:
Listing 6: Using
the FooterFormat
Class: TextWithFooter
import java.awt.*;
import java.awt.font.*;
import java.awt.geom.*;
import java.awt.print.*;
import java.text.*;
/**
* The TextWithFooter application uses the
* page setup and print dialogs to print
* a hunk of text. A FooterFormat class
* is used to print a footer on the page.
*/
public class TextWithFooter implements Printable { |
/**
* The text to be printed.
*/
private static final String mText =
"Four score and seven years ago our fathers brought forth on this "
+ "continent a new nation, conceived in liberty and dedicated to the "
+ "proposition that all men are created equal. Now we are engaged in "
+ "a great civil war, testing whether that nation or any nation so "
+ "conceived and so dedicated can long endure. We are met on a great "
+ "battlefield of that war. We have come to dedicate a portion of "
+ "that field as a final resting-place for those who here gave their "
+ "lives that that nation might live. It is altogether fitting and "
+ "proper that we should do this. But in a larger sense, we cannot "
+ "dedicate, we cannot consecrate, we cannot hallow this ground."
+ "The brave men, living and dead who struggled here have consecrated "
+ "it far above our poor power to add or detract. The world will "
+ "little note nor long remember what we say here, but it can never "
+ "forget what they did here. It is for us the living rather to be "
+ "dedicated here to the unfinished work which they who fought here "
+ "have thus far so nobly advanced. It is rather for us to be here "
+ "dedicated to the great task remaining before us--that from these "
+ "honored dead we take increased devotion to that cause for which "
+ "they gave the last full measure of devotion--that we here highly "
+ "resolve that these dead shall not have died in vain, that this "
+ "nation under God shall have a new birth of freedom, and that "
+ "government of the people, by the people, for the people shall "
+ "not perish from the earth.";
/**
* Our text in a form for which we can obtain a
* AttributedCharacterIterator.
/
private static final AttributedString mStyledText = new AttributedString(mText);
/**
* Print a single page containing some sample text.
*/
static public void main(String args[]) { |
/* Get the representation of the current printer and
* the current print job.
*/
PrinterJob printerJob = PrinterJob.getPrinterJob();
/* Use a PageFormat that also prints a footer.
*/
PageFormat format = new FooterFormat();
/* Let the user choose a paper size and orientation.
*/
format = printerJob.pageDialog(format);
/* Build a book containing pairs of page painters (Printables)
* and PageFormats. This example has a single page containing
* text.
*/
Book book = new Book();
book.append(new TextWithFooter(), format);
/* Set the object to be printed (the Book) into the PrinterJob.
* Doing this before bringing up the print dialog allows the
* print dialog to correctly display the page range to be printed
* and to dissallow any print settings not appropriate for the
* pages to be printed.
*/
printerJob.setPageable(book);
/* Show the print dialog to the user. This is an optional step
* and need not be done if the application wants to perform
* 'quiet' printing. If the user cancels the print dialog then false
* is returned. If true is returned we go ahead and print.
*/
boolean doPrint = printerJob.printDialog();
if (doPrint) { |
try {
printerJob.print();
} catch (PrinterException exception) {
System.err.println("Printing error: " + exception);
}
}
}
/**
* Print a page of text.
*/
public int print(Graphics g, PageFormat format, int pageIndex) throws PrinterException {
/* We'll assume that Jav2D is available. Create a copy
* of it so that we can pass the original Graphics
* instance to the PageFormat instance.
*/
Graphics2D g2d = (Graphics2D) g.create();
/* Move the origin from the corner of the Paper to the corner
* of the imageable area.
*/
g2d.translate(format.getImageableX(), format.getImageableY());
/* Set the text color.
*/
g2d.setPaint(Color.black);
/* Use a LineBreakMeasurer instance to break our text into
* lines that fit the imageable area of the page.
*/
Point2D.Float pen = new Point2D.Float();
AttributedCharacterIterator charIterator = mStyledText.getIterator();
LineBreakMeasurer measurer = new LineBreakMeasurer(charIterator, g2d.getFontRenderContext());
float wrappingWidth = (float) format.getImageableWidth();
while (measurer.getPosition() < charIterator.getEndIndex()) { |
TextLayout layout = measurer.nextLayout(wrappingWidth);
pen.y += layout.getAscent();
float dx = layout.isLeftToRight() ? 0 : (wrappingWidth - layout.getAdvance());
layout.draw(g2d, pen.x + dx, pen.y);
pen.y += layout.getDescent() + layout.getLeading();
}
g2d.dispose();
g2d = null;
/* Calling the PageFormat is not part of the printing API,
* but it is a useful convention. In this example PageFormat
* does not implement Printable and so it is not invoked here.
* In later examples, PageFormat will implement Printable.
*/
try {
Printable formatPainter = (Printable) format;
formatPainter.print(g, format, pageIndex);
/* Nothing to do here. The PageFormat has nothing to print.
*/
} catch (ClassCastException exception) {
}
return Printable.PAGE_EXISTS;
}
The PrinterGraphics
Interface
The PrinterGraphics
interface was discussed briefly in the Printable interface discussion.
When a Printable's
print method is invoked, the first parameter is a Graphics instance that
implements the PrinterGraphics
interface. The print method can use the PrinterGraphics
interface to get a reference to the current PrinterJob.
In Listing 7, the FooterFormat
class from the previous section is extended so that it prints the document
name in the footer, along with the date and page number. The document name
is obtained from the PrinterJob.
Listing 7: Extending the FooterFormat
Class
<Example Under Development>
The Pageable
Arial
The Book
class, provided with the SDK 1.2 Printing API, is useful for applications
that have page-oriented contents. Such applications include word processors,
report generators, and page layout programs. Another class of programs
is canvas-oriented rather than page-oriented. These canvas-oriented applications
draw on a single large canvas which, at print time, needs to be split across
several pages. As an example of an application class that implements the
Pageable interface,
Listing 8 presents a Vista
class which can print a canvas over multiple pages. This class is very
simple and we will subclass it to make it useful in more specific environments,
such as Swing.
Listing 8: Printing Over Multiple
Pages: Vista
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.geom.Point2D;
import java.awt.print.Pageable;
import java.awt.print.PageFormat
import java.awt.print.Printable;
import java.awt.print.PrinterException;
/**
* A simple Pageable class that can
* split a large drawing canvas over multiple
* pages.
*
* The pages in a canvas are laid out on
* pages going left to right and then top
* to bottom.
*/
public class Vista implements Pageable { |
private int mNumPagesX;
private int mNumPagesY;
private int mNumPages;
private Printable mPainter;
private PageFormat mFormat;
/**
* Create a java.awt.Pageable that will print
* a canvas over as many pages as are needed.
* A Vista can be passed to PrinterJob.setPageable.
*
* @param width The width, in 1/72nds of an inch,
* of the vist's canvas.
*
* @param height The height, in 1/72nds of an inch,
* of the vista's canvas.
*
* @param painter The object that will drawn the contents
* of the canvas.
*
* @param format The description of the pages on to which
* the canvas will be drawn.
*/
public Vista(float width, float height, Printable painter, PageFormat format) { |
setPrintable(painter);
setPageFormat(format);
setSize(width, height);
}
/**
* Create a vista over a canvas whose width and height
* are zero and whose Printable and PageFormat are null.
*/
protected Vista() {
}
/**
* Set the object responsible for drawing the canvas.
*/
protected void setPrintable(Printable painter) { |
mPainter = painter;
}
/**
* Set the page format for the pages over which the
* canvas will be drawn.
*/
protected void setPageFormat(PageFormat pageFormat) { |
mFormat = pageFormat;
}
/**
* Set the size of the canvas to be drawn.
*
* @param width The width, in 1/72nds of an inch, of
* the vist's canvas.
*
* @param height The height, in 1/72nds of an inch, of
* the vista's canvas.
*/
protected void setSize(float width, float height) { |
mNumPagesX = (int) ((width + mFormat.getImageableWidth() - 1)/ mFormat.getImageableWidth());
mNumPagesY = (int) ((height + mFormat.getImageableHeight() - 1)/ mFormat.getImageableHeight());
mNumPages = mNumPagesX * mNumPagesY;
}
/**
* Returns the number of pages over which the canvas
* will be drawn.
*/
public int getNumberOfPages() {
return mNumPages;
}
protected PageFormat getPageFormat() {
return mFormat;
}
/**
* Returns the PageFormat of the page specified by
* pageIndex. For a Vista the PageFormat
* is the same for all pages.
*
* @param pageIndex the zero based index of the page whose
* PageFormat is being requested
* @return the PageFormat describing the size and
* orientation.
* @exception IndexOutOfBoundsException
* the Pageable does not contain the requested
* page.
*/
public PageFormat getPageFormat(int pageIndex) throws IndexOutOfBoundsException { |
if (pageIndex >= mNumPages) {
throw new IndexOutOfBoundsException();
}
return getPageFormat();
}
/**
* Returns the <code>Printable</code> instance responsible for
* rendering the page specified by <code>pageIndex</code>.
* In a Vista, all of the pages are drawn with the same
* Printable. This method however creates
* a Printable which calls the canvas's
* Printable. This new Printable
* is responsible for translating the coordinate system
* so that the desired part of the canvas hits the page.
*
* The Vista's pages cover the canvas by going left to
* right and then top to bottom. In order to change this
* behavior, override this method.
*
* @param pageIndex the zero based index of the page whose
* Printable is being requested
* @return the Printable that renders the page.
* @exception IndexOutOfBoundsException
* the Pageable does not contain the requested
* page.
*/
public Printable getPrintable(int pageIndex) throws IndexOutOfBoundsException { |
if (pageIndex >= mNumPages) {
throw new IndexOutOfBoundsException();
}
double originX = (pageIndex % mNumPagesX) * mFormat.getImageableWidth();
double originY = (pageIndex / mNumPagesX) * mFormat.getImageableHeight();
Point2D.Double origin = new Point2D.Double(originX, originY);
return new TranslatedPrintable(mPainter, origin);
}
/**
* This inner class's sole responsibility is to translate
* the coordinate system before invoking a canvas's
* painter. The coordinate system is translated in order
* to get the desired portion of a canvas to line up with
* the top of a page.
*/
public static final class TranslatedPrintable implements Printable { |
/**
* The object that will draw the canvas.
*/
private Printable mPainter;
/**
* The upper-left corner of the part of the canvas
* that will be displayed on this page. This corner
* is lined up with the upper-left of the imageable
* area of the page.
*/
private Point2D mOrigin;
/**
* Create a new Printable that will translate
* the drawing done by painter on to the
* imageable area of a page.
*
* @param painter The object responsible for drawing
* the canvas
*
* @param origin The point in the canvas that will be
* mapped to the upper-left corner of
* the page's imageable area.
*/
public TranslatedPrintable(Printable painter, Point2D origin) { |
mPainter = painter;
mOrigin = origin;
}
/**
* Prints the page at the specified index into the specified
* {@link Graphics} context in the specified
* format. A PrinterJob calls the
* Printableinterface to request that a page be
* rendered into the context specified by
* graphics. The format of the page to be drawn is
* specified by pageFormat. The zero based index
* of the requested page is specified by pageIndex.
* If the requested page does not exist then this method returns
* NO_SUCH_PAGE; otherwise PAGE_EXISTS is returned.
* The Graphics class or subclass implements the
* {@link PrinterGraphics} interface to provide additional
* information. If the Printable object
* aborts the print job then it throws a {@link PrinterException}.
* @param graphics the context into which the page is drawn
* @param pageFormat the size and orientation of the page being drawn
* @param pageIndex the zero based index of the page to be drawn
* @return PAGE_EXISTS if the page is rendered successfully
* or NO_SUCH_PAGE if pageIndex specifies a
* non-existent page.
* @exception java.awt.print.PrinterException
* thrown when the print job is terminated.
*/
public int print(Graphics graphics, PageFormat pageFormat, int pageIndex) throws PrinterException { |
Graphics2D g2 = (Graphics2D) graphics;
g2.translate(-mOrigin.getX(), -mOrigin.getY());
mPainter.print(g2, pageFormat, 1);
return PAGE_EXISTS;
}
}
}
The Vista
class is not very useful by itself. Listing 9 creates a subclass of Vista,
JComponentVista
that makes it easy to print Swing JComponents
over multiple pages:
Listing 9: Printing JComponents
over Multiple Pages: JComponentVista
import java.awt.*;
import java.awt.print.*;
import javax.swing.*;
public class JComponentVista extends Vista implements Printable {
private static final boolean SYMMETRIC_SCALING = true;
private static final boolean ASYMMETRIC_SCALING = false;
private double mScaleX;
private double mScaleY;
/**
* The Swing component to print.
*/
private JComponent mComponent;
/**
* Create a Pageable that can print a
* Swing JComponent over multiple pages.
*
* @param c The swing JComponent to be printed.
*
* @param format The size of the pages over which
* the componenent will be printed.
*/
public JComponentVista(JComponent c, PageFormat format) { |
setPageFormat(format);
setPrintable(this);
setComponent(c);
/* Tell the Vista we subclassed the size of the canvas.
*/
Rectangle componentBounds = c.getBounds(null);
setSize(componentBounds.width, componentBounds.height);
setScale(1, 1); |
}
protected void setComponent(JComponent c) {
mComponent = c;
}
protected void setScale(double scaleX, double scaleY) {
mScaleX = scaleX;
mScaleY = scaleY;
}
public void scaleToFitX() {
PageFormat format = getPageFormat();
Rectangle componentBounds = mComponent.getBounds(null);
double scaleX = format.getImageableWidth() /componentBounds.width;
double scaleY = scaleX;
if (scaleX < 1) {
setSize( (float) format.getImageableWidth(),
(float) (componentBounds.height * scaleY));
setScale(scaleX, scaleY);
}
}
public void scaleToFitY() {
PageFormat format = getPageFormat();
Rectangle componentBounds = mComponent.getBounds(null);
double scaleY = format.getImageableHeight() /componentBounds.height;
double scaleX = scaleY;
if (scaleY < 1) {
setSize( (float) (componentBounds.width * scaleX),(float) format.getImageableHeight());
setScale(scaleX, scaleY);
}
}
public void scaleToFit(boolean useSymmetricScaling) {
PageFormat format = getPageFormat();
Rectangle componentBounds = mComponent.getBounds(null);
double scaleX = format.getImageableWidth() /componentBounds.width;
double scaleY = format.getImageableHeight() /componentBounds.height;
System.out.println("Scale: " + scaleX + " " + scaleY);
if (scaleX < 1 || scaleY < 1) {
if (useSymmetricScaling) {
if (scaleX < scaleY) {
scaleY = scaleX;
} else {
scaleX = scaleY;
}
}
setSize( (float) (componentBounds.width * scaleX), (float) (componentBounds.height * scaleY) );
setScale(scaleX, scaleY);
)
}
public int print(Graphics graphics, PageFormat pageFormat, int pageIndex) throws PrinterException {
Graphics2D g2 = (Graphics2D) graphics;
g2.translate(pageFormat.getImageableX(), pageFormat.getImageableY());
Rectangle componentBounds = mComponent.getBounds(null);
g2.translate(-componentBounds.x, -componentBounds.y);
g2.scale(mScaleX, mScaleY);
boolean wasBuffered = mComponent.isDoubleBuffered();
mComponent.paint(g2);
mComponent.setDoubleBuffered(wasBuffered);
return PAGE_EXISTS; |
}
)
Next, JBrowser.java
(Listing 10) uses JComponentVista
to print an HTML page over several physical pages. JComponentVista's
scaling methods are also demonstrated in the example.
Listing 10: Printing HTML Pages:
JBrowser
import java.awt.*;
import java.awt.event.*;
import java.awt.print.*;
import java.awt.Window;
import java.io.*;
import java.util.*;
import javax.swing.*;
import javax.swing.event.*;
/**
* Uses JFrame to create a simple
browser with printing.
* User passes String URL (e.g.,
http://www.rbi.com)
* to set opening location. User
then may follow
* hyperlinks or type new preferred
location into
* provided JTextField.
*
* Scaling options are demonstrated
in this
* example. Scaling options may
be set from a
* submenu in the File menu or
by specified KeyStroke.
* A second menu track nn websites,
which user can
* reselect as destination using
mouse.
*/
public class JBrowser extends
JFrame {
private static final
int kNumSites = 20; //number of sites listed in JMenu "Last 20"
private static final int kDefaultX
= 640;
private static final int kDefaultY
= 480;
private static final int prefScale
= 0;
private static final String kScale2Label
= "2X Scale";
private static final String
kScaleFitLabel = "Scale to Fit";
private static final String
kScaleHalfLabel = "1/2 Scale";
private static final String
kScaleOffLabel = "Scaling Off";
private static final String
kScaleXLabel = "Scale by Width";
private static final String
kScaleYLabel = "Scale by Length";
private JEditorPane mainPane;
private String path;
private JButton goButton = new
JButton("Go");
private JComponentVista vista;
private JMenu fileMenu = new
JMenu("File", true);
private JMenu prefMenu = new
JMenu("Print Preferences", true);
private JMenu siteMenu = new
JMenu("Last 20", true);
private JRadioButtonMenuItem
scale2RadioBut = new JRadioButtonMenuItem(kScale2Label);
private JRadioButtonMenuItem
scaleFitRadioBut = new JRadioButtonMenuItem(kScaleFitLabel);
private JRadioButtonMenuItem
scaleHalfRadioBut = new JRadioButtonMenuItem(kScaleHalfLabel);
private JRadioButtonMenuItem
scaleOffRadioBut = new JRadioButtonMenuItem(kScaleOffLabel, true);
private JRadioButtonMenuItem
scaleXRadioBut = new JRadioButtonMenuItem(kScaleXLabel);
private JRadioButtonMenuItem
scaleYRadioBut = new JRadioButtonMenuItem(kScaleYLabel);
private JTextField pathField
= new JTextField(30);
private Vector siteMIVector
= new Vector();
public static void main(String[]
args) {
new JBrowser(args[0]);
}
public JBrowser(String url) {
super("JBrowser HTML
Printing Demo");
path = url;
addSite(path);
try {
mainPane = new JEditorPane(path);
} catch (IOException ex) {
ex.printStackTrace(System.err);
System.exit(1);
}
JMenuBar menuBar = new JMenuBar();
JPanel navPanel = new JPanel();
JMenuItem printMI = new JMenuItem("Print");
JMenuItem exitMI = new JMenuItem("Exit");
printMI.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_P,
Event.CTRL_MASK));
exitMI.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_X,
Event.CTRL_MASK));
scale2RadioBut.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_D,
Event.CTRL_MASK));
scaleFitRadioBut.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_F,
Event.CTRL_MASK));
scaleHalfRadioBut.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_H,
Event.CTRL_MASK));
scaleOffRadioBut.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_O,
Event.CTRL_MASK));
scaleXRadioBut.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_W,
Event.CTRL_MASK));
scaleYRadioBut.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_L,
Event.CTRL_MASK));
printMI.addActionListener(new
printMIListener());
exitMI.addActionListener(new
exitMIListener());
scaleXRadioBut.addActionListener(new
scaleXListener());
scaleYRadioBut.addActionListener(new
scaleYListener());
scaleFitRadioBut.addActionListener(new
scaleFitListener());
scaleHalfRadioBut.addActionListener(new
scaleHalfListener());
scale2RadioBut.addActionListener(new
scale2Listener());
pathField.addActionListener(new
pathFieldListener());
pathField.setText(path);
goButton.addActionListener(new
pathFieldListener());
ButtonGroup scaleSetGroup = new
ButtonGroup();
scaleSetGroup.add(scale2RadioBut);
scaleSetGroup.add(scaleFitRadioBut);
scaleSetGroup.add(scaleHalfRadioBut);
scaleSetGroup.add(scaleOffRadioBut);
scaleSetGroup.add(scaleXRadioBut);
scaleSetGroup.add(scaleYRadioBut);
prefMenu.add(scaleXRadioBut);
prefMenu.add(scaleYRadioBut);
prefMenu.add(scaleFitRadioBut);
prefMenu.add(scaleHalfRadioBut);
prefMenu.add(scale2RadioBut);
prefMenu.addSeparator();
prefMenu.add(scaleOffRadioBut);
fileMenu.add(prefMenu);
fileMenu.add(printMI);
fileMenu.addSeparator();
fileMenu.add(exitMI);
menuBar.add(fileMenu);
menuBar.add(siteMenu);
menuBar.add(pathField);
menuBar.add(goButton);
mainPane.setEditable(false);
mainPane.addHyperlinkListener(new
linkListener());
vista = new JComponentVista(mainPane,
new PageFormat());
addWindowListener(new BasicWindowMonitor());
setContentPane(new JScrollPane(mainPane));
setVisible(true);
setJMenuBar(menuBar);
setSize(kDefaultX, kDefaultY);
setVisible(true);
}
/*
* addSite method takes the String
url and adds it to the
* Vector siteMIVector and the
JMenu siteMenu.
*/
public void addSite(String url)
{
boolean beenThere =
false;
/*
* Cycle through the contents
of the siteMenu, comparing
* their labels to the string
to determine if there is
* redundancy.
*/
for(int i=0; i<siteMenu.getItemCount()
&& !beenThere; i++) {
JMenuItem site = siteMenu.getItem(i);
/*
* The String url, is compared
to the labels of the
* JMenuItems in already stored
in siteMIVector. If
* the string matches an existing
label, the older
* redundant element, at i, is
removed. The new JMenuItem
* site is inserted in the Vector
at 0. The updateMenu
* method is called to update
the "Last nn" menu accordingly
* and the "beenThere" boolean
trigger is set TRUE.
*/
if (site.getText().equals(url))
{
siteMIVector.removeElementAt(i);
siteMIVector.insertElementAt(site,
0);
updateMenu(siteMenu);
beenThere = true;
}
}
/*
* If the new JMenuItem site
has a unique string, then the
* addSite method handles it
as follows.
*/
if (!beenThere) {
/*
* If the "Last nn" menu has
reached kNumSites capacity,
* the oldest JMenuItem is removed
from the vector, enabling
* storage for the new menu item
and maintaining the specified
* capacity of the "Last nn"
menu.
*/
if (siteMenu.getItemCount()
>= kNumSites){
siteMIVector.removeElementAt(siteMIVector.size()-1);
}
/*
* A new JMenuItem is created
and a siteMenuListener added.
* It is added to the vector
then the menu is updated.
*/
JMenuItem site = new JMenuItem(url);
site.addActionListener(new siteMenuListener(url));
siteMIVector.insertElementAt(site,
0);
System.out.println("\n Connected
to "+ url);
updateMenu(siteMenu);
}
}
public void updateMenu(JMenu
menu) {
menu.removeAll();
for(int i=0; i<siteMIVector.size();
i++) {
JMenuItem mi = (JMenuItem)siteMIVector.elementAt(i);
menu.add(mi);
}
}
/*
*
* The ActionListener methods
*
*/
public class exitMIListener
implements ActionListener {
public void actionPerformed(ActionEvent
evt) {
System.out.println("\n
Killing JBrowser...");
System.out.println(" ...AHHHHHHHHHhhhhhhh...ya
got me...ugh");
System.exit(0);
}
}
public class linkListener implements
HyperlinkListener {
public void hyperlinkUpdate(HyperlinkEvent
ev) {
try {
if (ev.getEventType()
== HyperlinkEvent.EventType.ACTIVATED) {
mainPane.setPage(ev.getURL());
path = ev.getURL().toString();
pathField.setText(path);
addSite(path);
}
} catch (IOException ex) {
ex.printStackTrace(System.err);
}
}
}
public class pathFieldListener
implements ActionListener {
public void actionPerformed(ActionEvent
evt) {
System.out.println("\n
Switching from "+path+" to "+pathField.getText()+".");
path = pathField.getText();
try {
mainPane.setPage(path);
} catch (IOException ex){
ex.printStackTrace(System.err);
}
if (!path.equals("")) {
addSite(path);
}
}
}
public class printMIListener
implements ActionListener {
public void actionPerformed(ActionEvent
evt) {
PrinterJob pj = PrinterJob.getPrinterJob();
pj.setPageable(vista);
try {
if (pj.printDialog())
{
pj.print();
}
} catch (PrinterException e) {
System.out.println(e);
}
}
}
public class scale2Listener implements
ActionListener {
public void actionPerformed(ActionEvent
evt) {
vista = new JComponentVista(mainPane,
new PageFormat());
vista.setScale(2.0, 2.0);
}
}
public class scaleFitListener
implements ActionListener {
public void actionPerformed(ActionEvent
evt) {
vista = new JComponentVista(mainPane,
new PageFormat());
vista.scaleToFit(false)
}
}
public class scaleHalfListener
implements ActionListener {
public void actionPerformed(ActionEvent
evt) {
vista = new JComponentVista(mainPane,
new PageFormat());
vista.setScale(0.5, 0.5);
}
}
public class scaleOffListener
implements ActionListener {
public void actionPerformed(ActionEvent
evt) {
vista = new JComponentVista(mainPane,
new PageFormat());
}
}
public class scaleXListener implements
ActionListener {
public void actionPerformed(ActionEvent
evt) {
vista = new JComponentVista(mainPane,
new PageFormat());
vista.scaleToFitX();
}
}
public class scaleYListener implements
ActionListener {
public void actionPerformed(ActionEvent
evt) {
vista = new JComponentVista(mainPane,
new PageFormat());
vista.scaleToFitY();
}
}
public class siteMenuListener
implements ActionListener {
private String site;
public siteMenuListener(String
url) {
site = url;
}
public void actionPerformed(ActionEvent
evt) {
System.out.println("\n
Switching from "+path+" to "+site+".");
path = site;
try {
mainPane.setPage(path);
} catch (IOException ex){
ex.printStackTrace(System.err);
}
if (!path.equals("")) {
addSite(path);
}
pathField.setText(path);
}
}
}
The Pageable
interface can also be used to provide special effects on top of an existing
class that implements Pageable.
For example here in Listing 11, we present a class the prints multiple
pages from an existing Pageable
on to single page. This behavior is typically referred to an n-up.
Listing 11: Printing Over Multiple
Pages: Vista
<Example Under Development>
Comments
With an understanding that the preceding
is a just a draft, please send comments to:
tutorial.java.printing@rbi.com
|
|