Chapter 15. Printing and XPS

WPF provides powerful printing support, allowing you to use the majority of its graphical features in print as well as on-screen. Dynamic features such as animation and event handling don’t translate to static paper output, of course, but you can use any stationary graphics. You can also use framework services such as data binding and layout to construct output for printing.

For many years, Windows has used a print spool system based on the Win32 Enhanced Meta File (EMF) format. This does not support the full WPF rendering feature set, so a new format has been introduced: XPS, the XML Paper Specification. This enables WPF-based graphics to be sent for printing without any loss of fidelity.

XPS is a fixed-layout page description format, and not only is it the basis for WPF print spooling, but also you can use it as a standalone file format. For example, you might build an XPS file so that you can email it to someone as a preview of what the printed output will look like.

Whether your WPF application is sending output directly to the printer or creating an XPS file containing the output, you will use the XPS APIs. So, in order to look at printing in WPF, we must begin by looking at XPS.

XPS

XPS is an open specification[104] for a file format designed to hold printable output from a WPF application. As the name suggests, an XPS file describes exactly how the document should look on paper.

The XPS format has been designed to be easy to create and consume. It builds on two very widely supported standards: the ZIP file format and XML. There are a few binary elements of an XPS file, such as embedded fonts and bitmaps, but these also use common standards—OpenType and TrueType for fonts, and TIFF, PNG, JPEG, and WMP for bitmaps.

The way XPS uses ZIP and XML is very similar to the XML-based file formats introduced in Office 2007. This is no coincidence. All of these files share a common system, described by the Open Packaging Conventions.

Open Packaging Conventions

The Open Packaging Conventions (OPC) specification is part of the Office Open XML suite of standards. These standards are owned by ECMA.[105] OPC describes a consistent scheme for packaging multiple streams into a single ZIP file, and a standard mechanism by which one stream can refer to the contents of another stream. For example, in XPS, each page is represented by a stream of XML. If the page uses a bitmap, the bitmap would be stored in a separate stream, and the page XML would point to that stream with a relative URL. As well as defining the means by which one stream refers to another, the packaging conventions also define mechanisms for incorporating common file properties (title, creator, etc.), thumbnail images, and digital signatures.

OPC makes it very easy to inspect the contents of a file manually. If you change the extension of an XPS file to .zip, you can then extract its contents as you would any normal ZIP file. Although this offers a very useful way to learn about an OPC-based format by inspecting example files, it’s important not to presume too much about the exact structure of the file. Even though you might observe common patterns, you often cannot count on these. For example, XPS files often contain a FixedDocSeq.fdseq stream in the root of the package, which typically defines the overall structure of the document. Similarly, Word files typically contain a document.xml stream in the word subdirectory in which you will find the document contents. However, you should not rely on these parts always having these names, as the names are of no significance. Parts are always located by relationships or references embedded within other parts. Only one well-known part will always be present: a stream called _rels/.rel. Applications start by opening this, and work out where to go next from there. Example 15-1 shows the contents of this stream from an XPS file. (A couple of lines have been split because they are too long to fit across the page.)

Example 15-1. Package relationships
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<Relationships
    xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
  <Relationship Id="rId3"
     Type="http://schemas.openxmlformats.org/package/2006/relationships/metadata/
core-properties"
     Target="docProps/core.xml"/>
  <Relationship Id="rId2"
     Type="http://schemas.openxmlformats.org/package/2006/relationships/metadata/
thumbnail"
     Target="docProps/thumbnail.jpeg"/>
  <Relationship Id="rId1"
     Type="http://schemas.microsoft.com/xps/2005/06/fixedrepresentation"
     Target="FixedDocSeq.fdseq"/>
</Relationships>

This shows where to locate three parts of the document. Each is identified with a fixed URI. To find the thumbnail image for this file, for example, an application would open this well-known _rels/.relstream, locate the Relationshipelement with the relevant TypeURI, and then open the stream to which it refers with its Targetproperty.

So, although you can learn a lot about the structure of an OPC-based file by looking inside it, you should never rely on the parts having particular names or locations. You should always use relationships to locate the parts. Moreover, if you’re using XPS, this issue will typically be dealt with for you, because WPF will use relationships and locate parts on your behalf if you use the XPS-specific classes it provides.

XPS Document Classes

The classes for working with XPS documents are spread across several namespaces, because there are several different levels at which you may wish to work. Table 15-1 shows the various namespaces that contain XPS functionality, and the scenarios for which they are intended.

Table 15-1. Namespaces for working with XPS

Namespace

Purpose of classes

System.Windows.Documents

Contains classes that represent the logical internal structure of XPS documents, and which also add extra runtime functionality for constructing documents

System.Windows.Xps

Abstract API for creating XPS documents, for either printing or writing to disk

System.Windows.Xps.Serialization

Provides fine-grained control of how the XPS file’s contents are generated

System.Windows.Xps.Packaging

Provides access to package-level aspects of XPS, such as loading and saving XPS documents, or adding thumbnail images

System.IO.Packaging

Allows reading and writing of any OPC file (not limited to XPS)

XPS file structure is most directly represented by the classes in the System.Windows.Xps.Packaging namespace. If you want to work directly with the streams of data that make up an XPS file, these classes will give you the most control. However, they are not all that convenient to use because of their low-level nature. There is an even lower level beneath the XPS packaging classes: the System.IO.Packaging namespace implements OPC. This is the foundation on which the XPS packaging classes are built. Although you could use the lowest-level classes to open an XPS file or any other OPC file, they don’t give you any more control—they merely take more work. In practice, the XPS-specific packaging classes are as low-level as you need. We mention System.IO.Packaging here because the System.Windows.Xps.Packaging classes use it—some types from the lower-level packaging API crop up in the XPS packaging API.

The classes in System.Windows.Documents also reflect the physical structure of the file, but add higher-level WPF services such as data binding and layout. So, we will start by looking at these higher-level classes.

The basic structure of an XPS file is very simple: a single XPS file contains one or more documents, and each document consists of one or more pages. (In printing terms, you could think of an XPS file as analogous to a print job containing one or more documents.) The System.Windows.Documents namespace represents this structure with three classes. FixedDocumentSequence represents the set of documents in the file. This contains a collection of FixedDocument objects, one for each document. These in turn contain a collection of FixedPage objects, one for each page.

Tip

All of the XPS document structure classes begin with Fixed to make it clear that the formatting of any XPS document is frozen. This distinguishes these classes from the flow document classes, which are also in the System.Windows.Documents namespace.

If you unzip an XPS file, its physical structure will typically embody this logical structure. For example, Figure 15-1 shows the parts in an XPS file that correspond to the classes just discussed. (The file we dissected here happens to be the XPS 1.0 specification itself—it is distributed as an XPS file.) In the root is a file corresponding to the FixedDocumentSequence: FixedDocSeq.fdseq, in this case. (Remember, this root file may not always have this name. The reliable way to find it is to follow the link in the _rels/.rel file.) The root also contains a Documents folder, with a subdirectory for each document in the sequence.

Example XPS file contents
Figure 15-1. Example XPS file contents

This particular file contains just one document, so there is a single subdirectory, 1. This contains a FixedDoc.fdoc file, which would be represented by a corresponding FixedDocument object if you loaded the XPS file into memory. The directory also has a Pages subfolder. This contains a series of .fpage files, each corresponding to a FixedPage object. Each page file contains graphical elements describing the exact appearance of the page.

All three levels—fixed document sequence, fixed document, and fixed page—are stored as XAML. We will now look at the classes WPF supplies for working with each of these structural levels, and the corresponding XAML in the XPS file.

FixedDocumentSequence

Logically, a FixedDocumentSequence is a collection of FixedDocument objects. Very often, it will contain just one FixedDocument, but the structure supports any number. Although this logical structure is simple, the details are slightly more involved. Example 15-2 shows the steps required to create a FixedDocumentSequence and add a single FixedDocument to it.

Example 15-2. Creating a FixedDocumentSequence containing a FixedDocument
FixedDocumentSequence fds = new FixedDocumentSequence(  );
FixedDocument doc = new FixedDocument(  );

DocumentReference docReference = new DocumentReference(  );
docReference.SetDocument(doc);

fds.References.Add(docReference);

The slightly surprising part of this example is that we are required to wrap the FixedDocument in a DocumentReference object. Surely it would be simpler if documents could be added directly to a collection in some property of the document sequence. However, the XAML in Example 15-3 offers a clue to why this would not be a good idea. This is the FixedDocSeq.fdseq stream from an XPS file—the serialized version of a FixedDocumentSequence.

Example 15-3. FixedDocumentSequence XAML
<FixedDocumentSequence xmlns="http://schemas.microsoft.com/xps/2005/06">
  <DocumentReference Source="/Documents/1/FixedDoc.fdoc"/>
</FixedDocumentSequence>

If the documents were held directly inside a property of the fixed document sequence, they would need to appear inline in this XAML. If this approach were used consistently throughout the file, the fixed pages would appear inline, too—the entire structure of the document would be stored in one huge XAML stream. This would be unwieldy for large documents, particularly for a viewer that wants to start displaying a document before the file is completely downloaded. To avoid having one huge XAML stream for the whole document, each FixedPage and each FixedDocument gets its own stream. (Separation of documents is typically less critical than separation of pages because it’s more common to have a high page count than a large number of documents in a single file, but it’s more consistent to have distinct streams at all levels.)

The purpose of the DocumentReference is to provide an extra level of indirection that enables this separation. The exact location of the relevant XAML stream in the file is determined by a relative URL—the Source property of the DocumentReference, in this case. The physical structure is not required to mirror the logical structure, even though it often does in practice.

Example 15-2 showed how to create from scratch a FixedDocumentSequence containing a FixedDocument. You can also use these classes to inspect an existing XPS file—Example 15-4 shows how to load a file from disk into these objects.

Example 15-4. Extracting FixedDocument objects from an XPS file
FixedDocumentSequence fds;
using (XpsDocument xpsDocumentFile = new XpsDocument("MyXpsDoc.xps",
                                                     FileAccess.Read)) {
    fds = xpsDocumentFile.GetFixedDocumentSequence(  );
}

foreach (DocumentReference docRef in fds.References) {
    FixedDocument document = docRef.GetDocument(false);

    ...use document...
}

The XpsDocument class reads the file and is able to return a FixedDocumentSequence object representing the top level of the file structure. We can then iterate through the DocumentReference objects in the References property. We call GetDocument, passing false to tell WPF that we don’t require it to reload the document from disk—WPF caches parts of the file in memory, and if you expect the file to have changed, you can pass true here instead. We don’t expect the file to have changed in between constructing the XpsDocument and calling GetDocument—you would normally pass true here only if you kept the document open for long enough that changes might have occurred. GetDocument returns the FixedDocument object representing the document to which the reference points.

FixedDocument

The FixedDocument class represents a single document. Logically, it consists of a sequence of pages, but as with the FixedDocumentSequence, an extra level of indirection enables its children to be defined in separate streams in the package.

Adding pages to a document is very similar to adding documents to a document sequence. Just as each document must be wrapped in a DocumentReference, each page must be wrapped in a PageContent object, although as Example 15-5 shows, the way you provide a PageContent with its FixedPage is slightly different. You need to call the IAddChild.AddChild method.

Example 15-5. Adding FixedPages to a FixedDocument
FixedDocument doc = new FixedDocument(  );

FixedPage page1 = new FixedPage(  );
PageContent page1Content = new PageContent(  );
((IAddChild)page1Content).AddChild(page1);
doc.Pages.Add(page1Content);

FixedPage page2 = new FixedPage(  );
PageContent page2Content = new PageContent(  );
((IAddChild) page2Content).AddChild(page2);
doc.Pages.Add(page2Content);

Example 15-6 shows how the FixedDocument created in Example 15-5 looks in XAML.

Example 15-6. FixedDocument in XAML
<FixedDocument xmlns="http://schemas.microsoft.com/xps/2005/06">
  <PageContent Source="Pages/1.fpage" />
  <PageContent Source="Pages/2.fpage" />
</FixedDocument>

Although we now have enough code to create a complete document, it’s not very interesting because all the pages are empty. For a document to be worth printing or viewing, the pages will need some content. This means working with the FixedPage class.

FixedPage

The XPS specification places very strict limitations on what is allowed inside a FixedPage. Page content must be built from just three element types: Canvas, Glyphs, and Path. Of course, Canvas has no intrinsic appearance—it is a layout panel. In XPS, it is used simply to group items, enabling a single transform to be applied to a collection of objects. So, we are left with just two elements for describing the page content: Glyphs, the lowest-level mechanism in WPF for presenting text, and Path, the general-purpose shape element.

At first glance, the FixedPage class seems not to enforce these restrictions. Example 15-7 populates a FixedPage with a Canvas containing a TextBlock. This appears to contravene the XPS specification, which requires us to represent text with Glyphs, not TextBlock.

Example 15-7. Adding content to a FixedPage
FixedPage page1 = new FixedPage(  );

Canvas content1 = new Canvas(  );
page1.Children.Add(content1);
TextBlock text1 = new TextBlock(  );
text1.Text = "Hello, world!";
text1.FontFamily = new FontFamily("Palatino Linotype");
text1.FontSize = 50;
Canvas.SetLeft(text1, 100);
Canvas.SetTop(text1, 200);
content1.Children.Add(text1);

In fact, the FixedPage class fully supports the XPS specification. But instead of imposing restrictions on the FixedPage object’s contents, WPF meets the XPS requirements by converting page content into the lower-level types when necessary. This conversion occurs when you print, or when you write a FixedDocument into an XPS file. Example 15-8 shows the results of the conversion.

Example 15-8. Text content converted into Glyphs in XPS file
<FixedPage xmlns="http://schemas.microsoft.com/xps/2005/06"
  xmlns:x="http://schemas.microsoft.com/xps/2005/06/resourcedictionary-key"
  xml:lang="en-us" Width="816" Height="1056">
  <Glyphs OriginX="100" OriginY="252.49" FontRenderingEmSize="50"
       FontUri="/Resources/53698881-83d0-479f-a8a4-5e5a58691f15.ODTTF"
       UnicodeString="Hello, world!" Fill="#FF000000" />
</FixedPage>

The TextBlock has been replaced by a Glyphs element. And, because WPF has determined that the Canvas we added was serving no purpose beyond positioning the text, it has omitted that entirely.

Tip

This illustrates that the conversion to XPS is a one-way trip. WPF preserves just the information required to create the right appearance and discards everything else. You cannot reconstruct the original UI from the XPS file—there is no way to tell from Example 15-8 that it started life as a TextBlock. The output would have looked exactly the same if we had used a Label or a Glyphs element in Example 15-7 to create the text. So, if you load a generated XPS file back in, its structure will usually not be the same as the original—only the appearance will be preserved.

Note that although the OriginX property matches the position we specified with a call to Canvas.SetLeft, the OriginY property is slightly different from the position we set. This is because the attached Canvas.Top property indicates where the top of an element’s bounding box should be, whereas the OriginY property indicates the position of the text baseline for the Glyphs.

The FixedPage itself has a Width and Height of 816 × 1056. As always in WPF, these are in units of device-independent pixels, which are 1/96th of an inch in size. So these dimensions correspond to 8.5 × 11 inches. This is the default size for a FixedPage, even if you live in a country where, say, A4 is more popular. You are free to set any size you like, although if you are generating XPS for the purposes of printing, you would normally set the size to match the target media, which is the topic of the next section.

Page sizing

FixedPage allows you to specify three sizes for your page. The Width and Height properties are obvious enough—these specify the size of the paper for which the output is intended. The other two sizes are necessary to deal with the technical limitations of printing.

Most printers cannot print right to the edge of the paper. Control over the paper position is somewhat approximate, so printers always leave a blank area near the edge of the paper to ensure that they don’t attempt to deposit ink or toner onto the rollers that feed the paper. A FixedPage can designate the area of the page in which it is intending to print by setting the ContentBox property. As Figure 15-2 shows, the ContentBox typically identifies an area smaller than the physical page.

ContentBox indicating location of printable content
Figure 15-2. ContentBox indicating location of printable content

By default, this is just information—setting ContentBox does not imply any particular behavior. However, a helpful XPS viewer application could use this to check that a document’s content fits into the printable area of the target printer, and scale the output to fit if necessary or at least alert users of a potential problem.

The final page size measure relates to the standard workaround for printing to the edge of the page. If you really need ink all the way to the edge of a page, you simply print onto a larger sheet of paper and then trim it down. As long as the printable area of the printer is bigger than the size to which you will be trimming, you will be able to print across the entire area of your final output. However, it’s not quite as simple as printing something the exact size of the final, trimmed page—if you did that, even the slightest paper misalignment during the trimming process would result in an annoying white gap at the edge of the page. The standard solution to this is to print a slightly larger image than is required. This is referred to as bleed—the content bleeds out of the page area and into the area destined to be trimmed. Often, the bleed area also contains marks used in the print production process, such as crop lines and registration marks. The BleedBox indicates how large this area is, as Figure 15-3 shows.

BleedBox indicates printing outside of target page size
Figure 15-3. BleedBox indicates printing outside of target page size

Tip

If the bleed area is being used only to hold production features such as printing marks, the content box may still be smaller than the physical page size, as shown in Figure 15-3. However, if the bleed box is being used in order to allow ink all the way to the edge of the page, this implies that the page is completely full, so the content box will fill the whole page.

Again, the BleedBox is usually just for information. But it could be important information for a print bureau producing the finished output—it could use this to verify that everything is set up correctly before committing to a print run.

Page content limitations

It is convenient to be able to add any WPF content to a FixedPage and have it automatically converted to the element types supported in XPS. However, this conversion process has its limitations. WPF offers a couple of features that cannot be represented directly with the limited repertoire available in an XPS file’s FixedPage: 3D and bitmap effects.

You can use these features in a FixedPage, but if you print the page or save it to an XPS file, then all 3D content, and all elements that use the BitmapEffect property, will be converted into bitmaps. They will be represented in the FixedPage as a Path object with a rectangular shape and a bitmap fill. This does not guarantee perfect fidelity—the image quality for such features is only as good as the resolution of the generated bitmap.

If you want to print content that uses effects such as drop shadows, you might be better off trying to approximate the effects you require using opacity masks and gradient brushes. Although these are not as convenient as the bitmap effects, they can be represented in an XPS file without any loss of fidelity.

Fonts, bitmaps, and other resources

The Glyphs element in Example 15-8 indicates the font through its FontUri property. This does not name the font—instead it refers to a font file embedded in the XPS file. Fonts, bitmaps, thumbnails, color profiles, and any other necessary resources are embedded as separate streams in the package. These are then referred to by path. For example, the FontUri in Example 15-8 is:

/Resources/53698881-83d0-479f-a8a4-5e5a58691f15.ODTTF

This indicates that the XPS package contains a Resources folder, and that this contains an embedded font file with the specified name. In general, you do not need to worry about creating these resources, unless you choose to work with the lower-level System.Windows.Xps.Packaging API. If you print or save a fixed document using the higher-level API, WPF adds the necessary resources for you.

Warning

It is important to be aware that WPF can automatically embed font files, or subsets of a font file. It does this to guarantee that a document can be displayed or printed correctly. However, it is your responsibility to ensure that you do not generate XPS files that contravene font licensing terms.

We have now seen all the high-level classes that represent the structure of an XPS document. We saw in Example 15-4 how to read an existing XPS file off the disk and into a tree of objects representing its structure. But what about the creation process, where we build an XPS document structure, and want to either print it or save it to disk? For this, we must use the XpsDocumentWriter class.

Generating XPS Output

Whether you are printing, or writing your output to an XPS file, you can use the same API: the XpsDocumentWriter class. If you wish to print, you can obtain one of these from the printing API, as Example 15-9 shows.

Example 15-9. Obtaining an XpsDocumentWriter for printing
PrintDocumentImageableArea imageArea = null;
XpsDocumentWriter xpdw = PrintQueue.CreateXpsDocumentWriter(ref imageArea);
if (xpdw != null) {
    ...provide XpsDocumentWriter with output here...
}

This will cause the standard print dialog to be shown, allowing the user to select a printer. If the user cancels the dialog, CreateXpsDocumentWriter returns null, but otherwise it returns an XpsDocumentWriter, along with an object that describes the size of the target’s paper and the margins of the printable area. If you wish to exercise more control over the print dialog and printer selection, there are several variations on this theme, described later in this chapter.

If you wish to send your output to an XPS file instead of a printer, you can obtain the document writer using the code shown in Example 15-10.

Example 15-10. Obtaining an XpsDocumentWriter for file output
using (XpsDocument xpsFile = new XpsDocument(xpsOutputPath, FileAccess.Write)) {
    XpsDocumentWriter xpdw = XpsDocument.CreateXpsDocumentWriter(xpsFile);

    ...provide XpsDocumentWriter with output here...

}

Once you have obtained an XPS document writer, you can use the same code whether you are writing to an XPS file or to a printer. For clarity, we will use the term printing to refer to both kinds of output; unless otherwise specified, any discussion of printing in the following sections also applies to generating XPS files.

You can provide an XpsDocumentWriter with content in many forms. For most of the supported content types, you call a suitable overload of the Write method. This is a one-shot process: once you’ve called Write, it will print the document straight away, and any further calls to Write will throw an exception. The following sections describe the various types of content accepted by a document writer.

Printing Fixed Documents

XpsDocumentWriter offers overloads of its Write method that accept a FixedPage, a FixedDocument, or a FixedDocumentSequence. These allow you to submit a single page, a whole document, or a job containing several documents for printing, respectively. If you want to print more than one fixed page you must pass either a FixedDocument or a FixedDocumentSequence, because you get to call the Write method only once. Example 15-11 creates and prints a FixedDocument.

Example 15-11. Creating and printing a FixedDocument
FixedDocument doc = new FixedDocument(  );

// Add first page to document
FixedPage page1 = new FixedPage(  );
PageContent page1Content = new PageContent(  );
((IAddChild)page1Content).AddChild(page1);
doc.Pages.Add(page1Content);


// Add content to first page
Canvas content1 = new Canvas(  );
page1.Children.Add(content1);
TextBlock text1 = new TextBlock(  );
text1.Text = "Hello";
text1.FontSize = 50;
Canvas.SetLeft(text1, 100);
Canvas.SetTop(text1, 200);
content1.Children.Add(text1);


// Add second page to document
FixedPage page2 = new FixedPage(  );
PageContent page2Content = new PageContent(  );
((IAddChild) page2Content).AddChild(page2);
doc.Pages.Add(page2Content);


// Add content to second page
Canvas content2 = new Canvas(  );
page2.Children.Add(content2);
TextBlock text2 = new TextBlock(  );
text2.Text = "World";
text2.FontSize = 50;
Canvas.SetLeft(text2, 100);
Canvas.SetTop(text2, 200);
content2.Children.Add(text2);


// Print document
PrintDocumentImageableArea imageArea = null;
XpsDocumentWriter xpsdw = PrintQueue.CreateXpsDocumentWriter(ref imageArea);
if (xpsdw != null) {
    xpsdw.Write(doc);
}

This builds a FixedDocument with two FixedPages. The first contains the text “Hello,” and the second contains the text “World”. The user will be shown the print dialog at the point at which the code asks PrintQueue for the document writer. The print job will be submitted to the print queue when it calls Write—when that method returns, print spooling will already be complete, and the job will be in the Windows print queue for the target printer.

If your FixedPage objects use any WPF element types other than Canvas, Glyphs, or Path, the conversion to these simple element types occurs when you call the XpsDocumentWriter.Write method.

Printing Visuals

You do not need to create FixedDocument or FixedPage objects explicitly. WPF is able to generate a fixed page automatically from any object that derives from Visual. Because Visual is the base class of all WPF user interface elements, this means you can print any element. It will even accept elements that are already part of a visual tree, enabling you to print a screenshot of a user interface.

Tip

Unlike the ordinary bitmap-based screenshots you get by pressing PrtScn or Alt-PrtScn in Windows, these screenshots will be resolution-independent. Also, they aren’t screenshots in the sense of being a direct copy of what’s visible on-screen—they will contain your application’s visuals and nothing else, even if your application is currently obscured by other programs.

To print a Visual, just pass it to the relevant overload of the Write method. Example 15-12 shows how to print a TextBlock element.

Example 15-12. Printing a single Visual
PrintDocumentImageableArea area = null;
XpsDocumentWriter xpsdw = PrintQueue.CreateXpsDocumentWriter(ref area);
if (xpsdw != null) {
    TextBlock myVisual = new TextBlock(  );

    double leftMargin = area.OriginWidth;
    double topMargin = area.OriginHeight;
    double rightMargin = area.MediaSizeWidth - area.ExtentWidth - leftMargin;
    double bottomMargin = area.MediaSizeHeight - area.ExtentHeight - topMargin;
    myVisual.Margin = new Thickness(leftMargin, topMargin,
                                    rightMargin, bottomMargin);

    myVisual.Text = "Hello, world";

    Size outputSize = new Size(area.MediaSizeWidth, area.MediaSizeHeight);
    myVisual.Measure(outputSize);
    myVisual.Arrange(new Rect(outputSize));
    myVisual.UpdateLayout(  );

    xpsdw.Write(myVisual);
}

This example performs layout on the TextBlock explicitly. WPF does this automatically for elements in a user interface, but because this TextBlock is not hosted in a window, it is our responsibility to perform the layout. Otherwise, it would not have a valid width and height, so it would be invisible when we try to print it. If we were trying to print an element that was already in the visual tree of a window, this layout step would not be necessary.

Note that the margin and size of the TextBlock have been based on the page size information in the PrintDocumentImageableArea returned by PrintQueue, ensuring that the TextBlock doesn’t fall into the unprintable area around the edge of the paper. PrintDocumentImageableArea reports the total page size with its MediaSizeWidth and MediaSizeHeight properties. The top-left position of the printable area within the page is indicated by the OriginWidth and OriginHeight properties. The ExtentWidth and ExtentHeight properties describe the size of the printable area.

Although basing the print margins on the physical capabilities of the printer guarantees to make full use of the available space, it means that output will vary from one printer to another. In practice, it’s common to choose fixed margins with sufficiently conservative values that the content is likely to fit with any printer’s printable region. Example 15-13 shows modifications to Example 15-12 that hardcode the margins. For some applications it may be appropriate to provide the user with a way to modify the margins; WPF doesn’t have a built-in page setup dialog,[106] so it’s up to you how to present such settings.

Example 15-13. Setting fixed print margins
...

TextBlock myVisual = new TextBlock(  );

double leftMargin = 96;    // 1 inch
double topMargin = 144;    // 1.5 inches
double rightMargin = 96;   // 1 inch
double bottomMargin = 144; // 1.5 inches

// Making sure that we're inside the physical
// limitations of the printer

Debug.Assert(leftMargin >= area.OriginWidth);
Debug.Assert(topMargin >= area.OriginHeight);
Debug.Assert(rightMargin <= area.OriginWidth + area.ExtentWidth);
Debug.Assert(topMargin <= area.OriginHeight + area.ExtentHeight);

myVisual.Margin = new Thickness(leftMargin, topMargin,
                                    rightMargin, bottomMargin);

...

Tip

The use of Debug.Assert in Example 15-13 is for illustration only. In practice, there is no one correct way to respond if the configured margins fail to place the content entirely within the printable area. An application might show the user a warning dialog the first time the problem occurs for a particular document. Alternatively, it might choose to adjust the content to fit.

Because the overload of the Write method used in Example 15-12 accepts any object that has Visual as a base class, we are free to pass in whole trees of objects. For example, Grid derives from Visual, so you can pass any Grid, and it will print the grid and all its contents.

However, if you need to print multiple pages, you need to take a slightly different approach. As with the previous examples, you are allowed to call this overload of the Write method only once, meaning you can print only a single page. If you want to print multiple pages as visuals, you need to use a different method of the XpsDocumentWriter class: CreateVisualsCollator.

CreateVisualsCollator returns a SerializerWriterCollator object. This offers a Write method that accepts a visual, just like XpsDocumentWrite.Write. The difference is that you can call it several times in a row, once for each page. Example 15-14 uses this technique to print 10 pages, with a TextBlock showing each page’s number.

Example 15-14. Printing multiple Visuals
PrintDocumentImageableArea area = null;
XpsDocumentWriter xpsdw = PrintQueue.CreateXpsDocumentWriter(ref area);
if (xpsdw != null) {
    SerializerWriterCollator c = xpsdw.CreateVisualsCollator(  );
    c.BeginBatchWrite(  );
    for (int i = 1; i <= 10; ++i) {
        TextBlock tb = new TextBlock(  );
        tb.Text = i.ToString(  );
        tb.TextAlignment = TextAlignment.Center;
        tb.VerticalAlignment = VerticalAlignment.Center;
        tb.FontSize = 500;
        tb.FontFamily = new FontFamily("Verdana");

        tb.Margin = new Thickness(area.OriginWidth, area.OriginHeight, 0, 0);
        Size outputSize = new Size(area.ExtentWidth, area.ExtentHeight);
        tb.Measure(outputSize);
        tb.Arrange(new Rect(outputSize));
        tb.UpdateLayout(  );

        c.Write(tb);
    }
    c.EndBatchWrite(  );
}

We must indicate the start and end of the range of pages by calling BeginBatchWrite and EndBatchWrite so that WPF knows when to start and complete the print spooling process. Once you have called EndBatchWrite, you cannot perform any more output with either the collator or the document writer.

Printing with Document Paginators

Some WPF classes have an intrinsic ability to split their content into pages. The FlowDocument, FixedDocument, and FixedDocumentSequence classes all advertise this capability by implementing the IDocumentPaginatorSource interface. XpsDocumentWriter can work with self-paginating objects directly, which can make multipage printing much simpler than the previous example. Example 15-15 shows a function that you can use to print an instance of any type that implements IDocumentPaginatorSource.

Example 15-15. Printing with DocumentPaginator
public static void PrintPaginatedDocument(IDocumentPaginatorSource dps) {
    PrintDocumentImageableArea area = null;
    XpsDocumentWriter xpsdw = PrintQueue.CreateXpsDocumentWriter(ref area);
    if (xpsdw != null) {
        xpsdw.Write(dps.DocumentPaginator);
    }
}

If the document paginator source is a fixed document or a fixed document sequence, Example 15-15 will generate an output page for each fixed page in the source. This is not usefully different from just passing the fixed document or fixed document sequence directly to the Write overload that accepts those types, so in practice, you will not usually print fixed documents this way. However, if the source document is a FlowDocument (which was described in Chapter 14), it will not have a fixed idea of how many pages it contains. In this case, the paginator is more useful, because you can use it to split the document into pages that fit the target media.

You might expect the FlowDocument pagination to be based automatically on the target page size. In fact, this is not the case: the paginator it returns defaults to a page size of 816 × 1056 pixels (i.e., 8.5 × 11 inches regardless of the real paper size). You can inspect or change the paginator’s page size using its PageSize property. If you are writing to an XPS file instead of printing, the generated XPS file will have pages of this size. If you are printing, the flow document will be split into pages of this size regardless of whether they fit onto the target paper. If you want to be sure that the flow document’s output will fit the available space offered by the target printer, you should use the information returned in the PrintDocumentImageableArea to adjust the PageSize property, as shown in Example 15-16.

Example 15-16. Setting a DocumentPaginator’s PageSize
PrintDocumentImageableArea area = null;
XpsDocumentWriter xpsdw = PrintQueue.CreateXpsDocumentWriter(ref area);
if (xpsdw != null) {
    DocumentPaginator paginator = dps.DocumentPaginator;
    paginator.PageSize = new Size(area.ExtentWidth, area.ExtentHeight);
    xpsdw.Write(paginator);
}

This will ensure that the flow document is broken into pages that fit into the available space, whatever the paper size may be.

There is a limitation with printing a FlowDocument in this way: you cannot add other features to the page, such as page numbers, without doing a little more work. You have two options: either you can implement your own DocumentPaginator as a wrapper around the one provided by the FlowDocument, or you can call into the provided paginator directly to retrieve the pages, and incorporate this into the content you require to represent the whole page.

Example 15-17 shows some code that uses the second technique to print a FlowDocument with page numbers.

Example 15-17. Printing a FlowDocument with page numbers
public static void PrintFlowDocWithPageNumbers(FlowDocument myFlowDoc) {
    PrintDocumentImageableArea area = null;
    XpsDocumentWriter xpsdw = PrintQueue.CreateXpsDocumentWriter(ref area);
    if (xpsdw != null) {
        IDocumentPaginatorSource dps = myFlowDoc;
        DocumentPaginator sourceFlowDocPaginator = dps.DocumentPaginator;

        const int HeaderFooterHeight = 30;
        sourceFlowDocPaginator.PageSize = new Size(area.ExtentWidth,
            area.ExtentHeight - 2 * HeaderFooterHeight);

        if (!sourceFlowDocPaginator.IsPageCountValid) {
            sourceFlowDocPaginator.ComputePageCount(  );
        }

        FixedDocument outputFixedDoc = BuildFixedDocument(myFlowDoc, area,
            sourceFlowDocPaginator, HeaderFooterHeight);


        xpsdw.Write(outputFixedDoc);
    }
}

This obtains an XpsDocumentWriter in the usual way. It then retrieves the document paginator for the flow document we wish to print. Next, it sets the PageSize on the paginator, specifying the full width of the printable page area, but reducing the height in order to allow space for a header and footer to be added. This ensures that the content, header, and footer fit within the printable area of the page.

Next, we make sure the paginator has an up-to-date page count—we changed the page size, which is likely to have changed the number of pages required for the document. If IsPageCountValid indicates that the page count is no longer up-to-date, we call ComputePageCount.

Finally, Example 15-17 builds a FixedDocument to hold the paginated output, combined with the header and footer, and prints it by calling the Write method of the XpsDocumentWriter. The document is created by the BuildFixedDocument helper method, which is shown in Example 15-18.

Example 15-18. Building a FixedDocument from a FlowDocument
static FixedDocument BuildFixedDocument(FlowDocument myFlowDoc,
        PrintDocumentImageableArea area, DocumentPaginator sourceFlowDocPaginator,
        int headerFooterHeight) {

    FixedDocument outputFixedDoc = new FixedDocument(  );

    for (int pageNo = 0; pageNo < sourceFlowDocPaginator.PageCount; ++pageNo) {
        Canvas pageCanvas = new Canvas(  );
        pageCanvas.Margin = new Thickness(192);

        AddHeaderAndFooter(myFlowDoc, area, sourceFlowDocPaginator,
                           headerFooterHeight, pageNo, pageCanvas);
        AddPageBody(sourceFlowDocPaginator,
                    headerFooterHeight, pageNo, pageCanvas);

        AddPageToDocument(area, outputFixedDoc, pageCanvas);

    }
    return outputFixedDoc;
}

This iterates through the pages provided by the paginator, creating a Canvas for each page. The Canvas will hold the page content, and the header and footer. We set the Margin on this canvas to ensure that it appears within the printable area of the page. The fixed margin of 2 inches (192 device-independent pixels) all around should be enough for any normal printer.

Next, we add the header and footer to the canvas, making sure they appear horizontally centered, and at the appropriate vertical position on the page. We add these with the AddHeaderAndFooter helper function, which is shown in Example 15-19.

Example 15-19. Adding the header and footer
static void AddHeaderAndFooter(FlowDocument myFlowDoc,
        PrintDocumentImageableArea area,
        DocumentPaginator sourceFlowDocPaginator,
        int headerFooterHeight, int pageNo, Canvas pageCanvas) {

    TextBlock header = new TextBlock(  );
    header.Text = "My Document";
    header.FontSize = 20;
    header.FontWeight = FontWeights.Bold;
    header.TextAlignment = TextAlignment.Center;
    header.Width = sourceFlowDocPaginator.PageSize.Width;
    pageCanvas.Children.Add(header);

    TextBlock footer = new TextBlock(  );
    footer.Text += "Page " + (pageNo + 1);
    footer.TextAlignment = TextAlignment.Center;
    footer.FontFamily = myFlowDoc.FontFamily;
    footer.Width = sourceFlowDocPaginator.PageSize.Width;
    Canvas.SetTop(footer, area.ExtentHeight - headerFooterHeight);
    pageCanvas.Children.Add(footer);
}

Finally, we need to incorporate the page content from the flow document. WPF offers a special element type, DocumentPageView, for hosting content from a particular page of a paginated document, which is used by the AddPageBody helper function shown in Example 15-20.

Example 15-20. Adding the page body
static void AddPageBody(DocumentPaginator sourceFlowDocPaginator,
        int headerFooterHeight, int pageNo, Canvas pageCanvas) {

    DocumentPageView dpv = new DocumentPageView(  );
    dpv.DocumentPaginator = sourceFlowDocPaginator;
    dpv.PageNumber = pageNo;
    Canvas.SetTop(dpv, headerFooterHeight);
    pageCanvas.Children.Add(dpv);
}

The Canvas defining the appearance of our page is now complete. The last thing the loop in Example 15-18 does is to create a FixedPage to host the Canvas, wrap it in a PageContent, and add that to the FixedDocument that will be returned to Example 15-17. This is accomplished by the AddPageToDocument helper shown in Example 15-21.

Example 15-21. Adding pages to the FixedDocument
static void AddPageToDocument(PrintDocumentImageableArea area,
        FixedDocument outputFixedDoc, Canvas pageCanvas) {

    FixedPage fp = new FixedPage(  );
    fp.Width = area.MediaSizeWidth;
    fp.Height = area.MediaSizeHeight;
    fp.Children.Add(pageCanvas);

    PageContent pc = new PageContent(  );
     ((IAddChild) pc).AddChild(fp);
    outputFixedDoc.Pages.Add(pc);
}

You can use the code in Example 15-17, along with its various helper functions, to print any flow document. It will add a header to each page with the text “My Document,” and a footer showing the page number.

Asynchronous Printing

Printing a large document can be a slow process. The Write methods shown in the previous sections are synchronous (i.e., they do not return until the document has been completely spooled out to the print queue). This can be bad news for the user—if you print from the main thread, the application will become unresponsive until printing finishes. Fortunately, WPF offers asynchronous versions of all of these Write methods. Each overload of Write has a corresponding WriteAsync method.

The WriteAsync methods return immediately. Printing will then progress during idle time. However, because WPF will be using the objects you passed in, you should make sure you don’t attempt to change them until printing is complete. The simplest way to ensure that is to create objects especially for printing. For example, build a FixedDocument that will be passed to an XPS document writer’s WriteAsync method, and which will never be used for anything else. (This rule also applies to each FixedPage object and all the objects that make up the page content.)

The XpsDocumentWriter will periodically raise the WritingProgressChanged event to tell you how far along it is. It will raise its WritingCompleted event once printing has completed spooling. The document may still be in the print queue at this point, so this event doesn’t necessarily mean the document is sitting on the printer awaiting collection; it just means that the application has finished generating the printer output—the application could exit without losing the output. Example 15-22 shows a version of Example 15-17 modified to use asynchronous printing.

Example 15-22. Asynchronous printing
void PrintPaginatedDocument(IDocumentPaginatorSource dps) {
    PrintDocumentImageableArea area = null;
    XpsDocumentWriter xpsdw = PrintQueue.CreateXpsDocumentWriter(ref area);
    xpsdw.WritingCompleted += OnPrinted;
    if (xpsdw != null) {
        xpsdw.WriteAsync(dps.DocumentPaginator);
    }

}

void OnPrinted(object sender, WritingCompletedEventArgs e) {
    MessageBox.Show("Printing completed!");
}

Sometimes users need to cancel print jobs. They can, of course, do this via the printing user interface in Windows. Alternatively, your application could call the CancelAsync method on the XpsDocumentWriter anytime before it signals completion. If the document is cancelled, the WritingCancelled event will be raised instead of the WritingCompleted event.

Everything in the preceding sections applies equally to generating XPS files and sending output to a printer. However, some tasks are specific to XPS file generation, and some apply only in printing scenarios. The next section describes parts of the .NET 3.0 API that are concerned solely with XPS file generation.

XPS File Generation Features

As we’ve seen, you can use the same code path to generate both printed output and XPS files by using the XpsDocumentWriter class. However, you can build some extra features into an XPS file that would not be useful for print output, but which can enhance its usefulness as a standalone file. Indeed, unless you plan to exploit some of these features, there’s probably not much point in adding a “save as XPS” feature to your application—Windows automatically offers basic XPS generation to any application that can print.

Tip

Any machine with either .NET 3.0 or the Microsoft XPS Essentials Pack[107] installed will have a “Microsoft XPS Document Writer” printer. Windows Vista has .NET 3.0 built in. You can install .NET 3.0 on Windows XP or Windows Server 2003. You can install the XPS Essentials Pack on these systems, and on Windows 2000 machines.

When an application “prints” to this printer, an XPS file is created. At the start of the print process, a dialog opens asking the user where he’d like to save the XPS file, and once printing is complete, the XPS viewer application will run, displaying the results.

This is very useful because it means that any print-enabled application can generate an XPS file. However, it is possible for applications to build a richer, more useful XPS file than this pseudoprinter can. An application knows things about its documents that the print system cannot, and can use this to enhance the XPS file. For example, XPS files can contain hyperlinks to allow easy navigation within documents. An application may also wish to control aspects of the file creation process, such as whether the compression strategy favors speed of decoding, or minimizing the file size.

Package-Level XPS API

The XpsDocumentWriter class is convenient in that it lets us use functionality not directly supported by the XPS file format itself. As we’ve seen, if we use non-XPS elements in a FixedPage, or we choose to work with Visual objects directly, the document writer will convert these objects to glyphs and paths for us. However, there is a price to pay: XpsDocumentWriter does not give you complete control over all XPS file features.

WPF provides a lower-level API that lets you work directly with the streams of an XPS file, giving you full control over all of the features, while still providing XPS-specific features that you don’t get from the low-level System.IO.Packaging classes, such as an intrinsic understanding of the parts and relationships required in an XPS file. Of course, the downside is that you have to do more work to build up a complete document from scratch. Fortunately, in a lot of cases, you can use both techniques: you can build the basic document using the higher-level API, and then use the lower-level API on the same file to add the XPS features you require.

The package-level API is defined in the System.Windows.Xps.Packaging namespace. Table 15-2 shows the types it provides for dealing with the core XPS structural elements, and the nearest equivalents in the high-level API.

Table 15-2. Package-level XPS API

Document structures

Type

System.Windows.Documents equivalent

XPS file

XpsDocument

None

Document structures

IXpsFixedDocumentSequenceReader, IXpsFixedDocumentSequenceWriter

FixedDocumentSequence

Document

IXpsFixedDocumentReader, IXpsFixedDocumentWriter

FixedDocument

Page

IXpsFixedPageReader, IXpsFixedPageWriter

FixedPage

Working with the package-level API requires a procedural style. Instead of building up a data structure in memory representing the documents and their pages, you issue a series of instructions in strict order, describing which things to write out. Example 15-23 shows how to create a document from scratch at this level.

Example 15-23. Generating an XPS file from scratch
using (XpsDocument xpsDoc = new XpsDocument("Out.xps", FileAccess.Write)) {
    IXpsFixedDocumentSequenceWriter sequenceWriter =
        xpsDoc.AddFixedDocumentSequence(  );
    IXpsFixedDocumentWriter docWriter = sequenceWriter.AddFixedDocument(  );
    IXpsFixedPageWriter pageWriter = docWriter.AddFixedPage(  );

    XmlWriter pageXml = pageWriter.XmlWriter;
    pageXml.WriteStartElement("FixedPage");
        pageXml.WriteAttributeString("xmlns",
                "http://schemas.microsoft.com/xps/2005/06");
        pageXml.WriteAttributeString("Width", "793.7");
        pageXml.WriteAttributeString("Height", "1122.5");
        pageXml.WriteAttributeString("xml:lang", "en-GB");

        pageXml.WriteStartElement("Path");
            pageXml.WriteAttributeString("Data",
                "M 10,550 L 396,164 782,550 396,936 z");
            pageXml.WriteAttributeString("Fill", "#ffff0000");

        pageXml.WriteEndElement(  );

    pageXml.WriteEndElement(  );

    pageWriter.Commit(  );
    docWriter.Commit(  );
    sequenceWriter.Commit(  );
}

Figure 15-4 shows how this generated page looks in the XPS viewer.

XPS file generated from scratch
Figure 15-4. XPS file generated from scratch

As you can see, you need to generate the raw XML for the fixed page content by hand if you are working at this level, and it is your responsibility to ensure that it conforms to the requirements laid out in the XPS specification. You cannot use the FixedPage class or any WPF elements with these APIs. Life gets even more complex if you wish to use text, as you need to generate embedded font resources. This is why it’s often preferable to use a hybrid approach. Example 15-24 shows how to generate the basic output with XpsDocumentWriter, and then use the package-level API to set a file property.

Example 15-24. Using both XPS API styles
using (Package xpsPackage = Package.Open("Out.xps", FileMode.Create,
                                         FileAccess.ReadWrite))
using (XpsDocument doc = new XpsDocument(xpsPackage)) {
    FixedPage page = new FixedPage(  );
    Path p = new Path(  );
    p.Data = StreamGeometry.Parse("M 10,550 L 396,164 782,550 396,936 z");
    p.Fill = Brushes.Red;
    page.Children.Add(p);1">

    TextBlock text = new TextBlock(  );
    text.Text = "XPS Output";
    text.FontSize = 150;
    page.Children.Add(text);

    XpsDocumentWriter dw = XpsDocument.CreateXpsDocumentWriter(doc);
    dw.Write(page);

    doc.CoreDocumentProperties.Description = "Some text and a red square";
}

There are some limitations to this technique, because you are leaving certain parts of the XPS file generation to the XpsDocumentWriter. For example, if you want fine control over the generated XML, you will have to stick to the lower-level technique. However, most of the techniques described in the following sections can use this hybrid approach, where the bulk of the XPS file is generated with XpsDocumentWriter, and then a few details are edited with the System.Windows.Xps.Packaging API.

Core Document Properties

OPC defines a representation for a set of document properties. These enable documents to supply common properties such as title, description, and keywords in a standard way. These properties are embedded in an XML stream in the package, with one element per property.

Several of the supported properties are defined by the Dublin Core Metadata Initiative,[108] a standard that defines how to represent certain common types of metadata in markup. There are also a number of properties not found in the Dublin Core, but which are common to all OPC-based file formats, including XPS and all of the Office Open XML file formats. Table 15-3 shows all the properties, indicating which are shared with the Dublin Core.

Table 15-3. Core document properties shared with Dublin Core

Property

Usage

In Dublin Core

Category

Categorization of document

No

ContentStatus

Status of document (e.g., “Draft”)

No

ContentType

Type of content, such as “Whitepaper” (note: this is not a MIME content type)

No

Created

Creation date

Yes

Creator

Entity primarily responsible for creating document

Yes

Description

Short description of the content

Yes

Identifier

An unambiguous reference to the document within a given context

Yes

Keywords

Set of keywords to facilitate searching

No

Language

Language in which document is written

Yes

LastModifiedBy

User who last modified the document

No

LastPrinted

Date on which the document was last printed

No

Modified

Date of last change

Yes

Revision

Revision number

No

Subject

Topic of document

Yes

Title

Name of document

Yes

Version

Version number

No

You’re not obliged to set any of these properties—set just those that make sense for your application. You do so using the XpsDocument class’s CoreDocumentProperties property. This contains an instance of the PackageProperties type that defines CLR properties corresponding to each available core document property. Example 15-25 shows how to use this.

Example 15-25. Setting core document properties
PackageProperties props = xpsOutput.CoreDocumentProperties;

props.Creator = "Ian Griffiths";
props.Title = "XPS Output";
props.Subject = "Example XPS output document";
props.Description = "XPS document generated by example from " +
    "the book 'Programming WPF' published by O'Reilly";
props.Category = "Demo output";
props.ContentStatus = "Final";
props.Keywords = "XPS; Demo; WPF";

Once you have set these properties, Windows Explorer is able to extract them, because it knows how to retrieve core properties from XPS files. Figure 15-5 shows how this looks. The labels Windows uses do not always correspond to the underlying property names. This is because Windows has some long-standing conventions for property names that predate the adoption of the Dublin Core properties by the OPC.

XPS core properties shown by Windows Explorer
Figure 15-5. XPS core properties shown by Windows Explorer

Windows Explorer allows the user to edit metadata on certain file formats. (Applications can register handlers to enable this editing. There is a built-in handler for XPS files.) You can see this in Figure 15-5: look at the “Content type” entry. Our code did not set this property, so the shell has shown an “Add text” prompt. If the user moves the mouse over this or any of the other editable properties, the value will take on a text-box-like appearance to let the user know she can edit the value. If she edits or adds values, a Save button appears, allowing her to commit the changes to disk. So be aware that the properties you write into an XPS file at creation time are not necessarily the properties that it will have for its lifetime.

Thumbnails

OPC defines a way to embed thumbnail bitmap images. A thumbnail is a small bitmap containing a preview of the document. Windows Vista can use this as the document’s icon in Explorer. Thumbnails enable a rough image of a document to be presented quickly, because they require very little processing—loading and rendering a small bitmap is likely to be faster than loading and rendering a FixedPage for all but the most trivial of documents.

The most common style of thumbnail is a small view of the first page of the document (or if the XPS file contains multiple fixed documents, the first page of the first document). XPS allows thumbnails to be provided for each page, but the default viewer does not use these, and neither the XPS Print Driver nor Microsoft Office’s XPS export feature generates them. In practice, a single thumbnail for the file should suffice.

To set the file’s thumbnail, you just call the XpsDocument object’s AddThumbnail method, passing an XpsImageType value to specify the image type. Although the XpsImageType enumeration lists four bitmap types, the specification requires thumbnails to be in either JPEG or PNG format, so you should not use the other types here—the enumeration includes the TIFF and WDP types because those are supported in other parts of an XPS document. AddThumbnail returns an object of type XpsThumbnail, which provides a method called GetStream. We then write the bitmap into that stream. It is our job to provide the raw JPEG or PNG byte stream. Example 15-26 shows how to generate a suitable bitmap and add it as a thumbnail.

Example 15-26. Adding a thumbnail to an XPS document
void AddThumbnailToXpsDocument(XpsDocument xpsOutput) {
    Size thumbnailSize = new Size(256, 256);
    Visual thumbnailVisual = CreateThumbnailVisual(thumbnailSize);

    XpsThumbnail docThumbnail = xpsOutput.AddThumbnail(XpsImageType.JpegImageType);
    WriteVisualAsThumbnail(thumbnailSize, thumbnailVisual, docThumbnail);

}
Visual CreateThumbnailVisual(Size thumbnailSize) {
    Grid myThumbnail = new Grid(  );
    myThumbnail.Background = new LinearGradientBrush(
        Colors.LightBlue, Colors.White, 90);

    TextBlock thumbText = new TextBlock(  );
    thumbText.FontSize = 50;
    thumbText.Text = "My XPS Document";
    thumbText.TextAlignment = TextAlignment.Center;
    thumbText.VerticalAlignment = VerticalAlignment.Center;
    thumbText.TextWrapping = TextWrapping.Wrap;
    myThumbnail.Children.Add(thumbText);

    myThumbnail.Measure(thumbnailSize);
    myThumbnail.Arrange(new Rect(new Point(  ), thumbnailSize));

    return myThumbnail;
}

void WriteVisualAsThumbnail(Size thumbnailSize, Visual thumbnailVisual,
                            XpsThumbnail thumbnail) {

    RenderTargetBitmap bmp = new RenderTargetBitmap(
        (int) thumbnailSize.Width, (int) thumbnailSize.Height,
        96, 96, PixelFormats.Default);
    bmp.Render(thumbnailVisual);

    JpegBitmapEncoder encoder = new JpegBitmapEncoder(  );
    encoder.Frames.Add(BitmapFrame.Create(bmp));

    encoder.Save(thumbnail.GetStream(  ));
}

Most of the code in this example is there to generate a JPEG bitmap. Only two lines of code are XPS-specific: the call to XpsDocument.AddThumbnail near the top, and the call to GetStream near the bottom.

This example generates a 256 × 256-pixel bitmap. The XPS specification doesn’t mandate any particular size, beyond saying that it should be “small.” However, Explorer in Windows Vista expects high-resolution icons to be 256 × 256, and because it is able to extract the thumbnail from an XPS and show it, 256 × 256 seems like a sensible default for an XPS thumbnail. Figure 15-6 shows how Windows Explorer displays the thumbnail created by Example 15-26 in its Large Icons view.

XPS document thumbnail shown by Explorer in Windows Vista
Figure 15-6. XPS document thumbnail shown by Explorer in Windows Vista

XPS documents can contain hyperlinks. These may be either internal links within the document or links to external sites. If you are building an XPS document using XpsDocumentWriter, the simplest way to generate hyperlinks is to use the WPF Hyperlink element.

In practice, navigation with XPS is slightly more complicated than what we saw in Chapter 11 because the XPS file format itself does not support the Hyperlink element directly. Instead, it requires you to set the FixedPage.NavigateUri attached property on the Canvas, Glyph, or Path elements that you would like to act as links. However, the XpsDocumentWriter can convert a Hyperlink into suitably annotated elements. It generates three elements for each hyperlink: the link text, a Path representing the underline, and a rectangular Path element with a transparent fill. This covers the whole area of the Hyperlink to ensure that it functions correctly as a click target even if the Hyperlink itself has a transparent background. Without this, the link would be much harder to click—clicking in, say, the space in the middle of a letter o or anywhere outside of the letter shapes would fail to activate the link.[109]

Adding a hyperlink to an external resource, such as a web site, is simple: just set the link’s NavigateUri to an absolute URL. But if you want a link on one page to be able to refer to another page, you must use a fragment URI (i.e., one beginning with a # character). The text following the # refers to a named element somewhere in the document.

For an element to be a valid target for a link, you must do two things. First, you must set the element’s Name property to match the fragment URI in the link. (For example, if the link’s NavigateUri is “#Heading_1” its target is an element named “Heading_1”.) Second, you must advertise the existence of the target with a LinkTarget object associated with the containing page’s PageContent object.

This second requirement exists to make sure XPS document viewers don’t need to parse every single page in order to find the link target. As we saw earlier, the FixedDocument part of an XPS file contains a sequence of PageContent elements describing the pages that make up the document, such as the one shown in Example 15-6. If you are using hyperlinks, this part must declare which pages contain which link targets. Example 15-27 shows such a document.

Example 15-27. FixedDocument with LinkTargets
<FixedDocument xmlns="http://schemas.microsoft.com/xps/2005/06">
  <PageContent Source="Pages/1.fpage" />
  <PageContent Source="Pages/2.fpage">
    <PageContent.LinkTargets>
      <LinkTarget Name="FirstItem" />
    </PageContent.LinkTargets>
  </PageContent>
  <PageContent Source="Pages/3.fpage">
    <PageContent.LinkTargets>
      <LinkTarget Name="SecondTarget" />
      <LinkTarget Name="Third" />
    </PageContent.LinkTargets>
  </PageContent>
</FixedDocument>

By declaring link targets in the FixedDocument part, it becomes possible for an XPS reader to follow internal links quickly. When the user clicks on a link, an XPS document viewer needs to scan only the FixedDocument to find the named link, and then open the containing page. Without this structure, it would need to scan every single page looking for the named element. This is not an optional performance enhancement—you are required to advertise link targets. Failure to do this will cause the links not to work.

Tip

This linking mechanism requires you to ensure that your fragment names are unique across the scope of the whole document. Indeed, the XPS specification recommends (but does not absolutely require) that if a single XPS file contains multiple documents, the fragment names should be unique across the scope of the whole file.

The code in Example 15-28 through Example 15-30 creates a document with a table of contents as its first page, containing a set of links to 10 more pages that make up the remainder of the document. Figure 15-7 shows how this first page will look. Clicking on any of these links in an XPS document viewer will jump directly to the relevant page.

Links in an XPS document
Figure 15-7. Links in an XPS document

Example 15-28 builds this FixedDocument. It begins by adding a page to contain the table of contents. This first page contains a TextBlock, which is initially empty. The for loop builds 10 more pages and creates hyperlinks to these pages, adding those links to the TextBlock on the first page, and building up the list of links as shown in Figure 15-7.

Example 15-28. Using hyperlinks: creating the document
public static FixedDocument MakeDocWithToc(  ) {
    FixedDocument doc = new FixedDocument(  );

    FixedPage tocPage = new FixedPage(  );
    TextBlock tableOfContents = new TextBlock(  );
    tocPage.Children.Add(tableOfContents);

    PageContent pc = new PageContent(  );
     ((IAddChild) pc).AddChild(tocPage);
    doc.Pages.Add(pc);

    for (int pageNumber = 1; pageNumber <= 10; ++pageNumber) {
        pc = MakePage(pageNumber);
        doc.Pages.Add(pc);

        Hyperlink link = MakeLink(pageNumber);
        tableOfContents.Inlines.Add(link);
        tableOfContents.Inlines.Add(new LineBreak(  ));
    }
    return doc;
}

This code relies on a helper function, MakePage, to generate each page. Although a single page can contain any number of targets, Example 15-29 keeps things simple by creating a page for each target. It adds a TextBlock to act as the target itself. The fragment IDs[110] take the form Page1, Page2, and so on.

Example 15-29. Using hyperlinks: creating pages with targets
static PageContent MakePage(int pageNumber) {
    PageContent pc;
    string fragmentId = "Page" + pageNumber;

    TextBlock tb = new TextBlock(  );
    tb.Text = "This is page " + pageNumber;
    tb.Name = fragmentId;

    FixedPage page = new FixedPage(  );
    page.Children.Add(tb);
    pc = new PageContent(  );
    ((IAddChild) pc).AddChild(page);

    LinkTarget lt = new LinkTarget(  );
    lt.Name = tb.Name;
    pc.LinkTargets.Add(lt);
    return pc;
}

We must also advertise the existence of the targets in the PageContent so that an XPS reader can find the targets without opening all of the FixedPage parts. This is why Example 15-29 creates a LinkTarget for each page.

After calling MakePage, Example 15-28 creates a Hyperlink for each page, using the MakeLink helper function shown in Example 15-30. This sets the NavigateUri property to a fragment URI matching the Name of the target.

Example 15-30. Using hyperlinks: creating the Hyperlink
static Hyperlink MakeLink(int pageNumber) {
    string fragmentId = "Page" + pageNumber;
    Run linkText = new Run("Go to page " + pageNumber);
    Uri linkTarget = new Uri("#" + fragmentId, UriKind.Relative);

    Hyperlink link = new Hyperlink(linkText);
    link.NavigateUri = linkTarget;

    return link;
}

If the FixedDocument created by Example 15-28 through Example 15-30 is written to an XPS file using an XpsDocumentWriter, the table of contents pages will contain the hyperlink, converted into Glyphs and Path elements as the XPS specification requires. If the user opens the file in the XPS viewer provided with .NET 3.0 and clicks on a link, it will take him straight to the target. Example 15-31 shows an excerpt from the first fixed page in the XPS file that the preceding example generates.

Example 15-31. Generated FixedPage with hyperlink
<FixedPage xmlns="http://schemas.microsoft.com/xps/2005/06"
 xmlns:x="http://schemas.microsoft.com/xps/2005/06/resourcedictionary-key"
 xml:lang="en-us" Width="816" Height="1056">
  <Glyphs OriginX="0" OriginY="12.95" FontRenderingEmSize="12"
          FontUri="/Resources/19775d40-e839-41d3-a9fe-ca4670f35840.ODTTF"
          UnicodeString="Go to page 1" Fill="#FF0000FF" />
  <Path Fill="#FF0000FF" Data="M0,13.65L69.19,13.65 69.19,14.34 0,14.34Z" />
...
  <Path FixedPage.NavigateUri="../FixedDocument.fdoc#Page1" Fill="#00000000"
        Data="F1M0,0L69.19,0 69.19,15.96 0,15.96Z" />
...
</FixedPage>

This shows only the elements for the first link. The Glyphs element presents the text of the link: “Go to page 1.” The first Path renders the underline—hyperlinks are displayed in the usual “blue with underline” style by default. The second Path defines a rectangle that covers the area occupied by the hyperlink, ensuring that the whole area is clickable. (It has a transparent fill, ensuring that it is invisible, but still presents a click target.) As you can see, this final Path element has the FixedPage.NavigateUri attached property. The XpsDocumentWriter has replaced our fragment URI with a relative URI pointing to the LinkTarget as declared in the fixed document part—this is the URI form that XPS readers will expect to see in the final output to represent a hyperlink. The result is that when the user clicks on a link in the contents page, he is taken to the corresponding target page.

Compression

When you create an XpsDocument, you have the option to specify the type of compression you would like to use. If you do not pass a CompressionOption, it will default to maximum compression. However, if you would like to favor speed over file size, you can pass in either Fast (as in Example 15-32) or SuperFast.

Example 15-32. Choosing fast compression
XpsDocument doc = new XpsDocument("out.xps", FileAccess.Write,
                                   CompressionOption.Fast);

In this section, we saw that many XPS features are of interest only when the output is destined for a file. The .NET 3.0 Framework also provides a number of features that are concerned solely with printing.

System.Printing

The System.Printing namespace defines types that provide printing services including managing settings of print jobs, discovering and selecting print queues, and configuring printers and print servers. We will explore the most commonly used types from this namespace.

PrintQueue

PrintQueue represents an output destination for printing—anytime you print something, you are sending it to a PrintQueue. The standard print dialog presents a list of available printers, and each printer in this list corresponds to a PrintQueue.

You have already seen several examples using PrintQueue—it offers a static CreateXpsDocumentWriter method that can return an XpsDocumentWriter. Although this method does not explicitly return a PrintQueue object, the writer it returns is implicitly bound to the PrintQueue of the printer the user selected in the print dialog. The examples shown earlier in this chapter call an overload of CreateXpsDocumentWriter that does not specify a target PrintQueue, causing a print dialog to be shown automatically. However, if you have a PrintQueue object, you can call one of the overloads that accepts a PrintQueue, in which case no dialog will be shown, and the document writer will target the specified queue. Example 15-33 shows one way to do this.

Example 15-33. Creating a document writer with a specific PrintQueue
LocalPrintServer local = new LocalPrintServer(  );
PrintQueue pq = local.DefaultPrintQueue;

XpsDocumentWriter xpsdw = PrintQueue.CreateXpsDocumentWriter(pq);

This example uses the default queue on the local machine. You can also obtain a PrintQueue from the PrintServer or the PrintDialog class. PrintQueue provides numerous properties (more than 60). Some of these provide static information about the queue, such as its name (FullName) and print speed (AveragePagesPerMinute). Others provide dynamic status information, such as IsPaperJammed and IsTonerLow.

PrintServer

PrintServer represents a machine offering one or more print queues. It provides a GetPrintQueues method that can return a collection of all the PrintQueue objects associated with the machine.

You can create a PrintServer by passing the machine name to the constructor. An alternative is a special derived class we saw in Example 15-33, called LocalPrintServer, which provides access to the local machine. This derived class adds a DefaultPrintQueue property, which returns the PrintQueue currently configured as the default print target.

GetPrintQueues offers overloads that enable you to filter the list of print queues. You can pass in an array of EnumeratedPrintQueueTypes values, specifying the kinds of printers you would like to see (e.g., print queues representing faxes, or print queues published in directory services).

If you already know exactly which print queue you want, you can call GetPrintQueue, passing in the queue name.

PrintServer even enables suitably privileged users to add and remove print queues. InstallPrintQueue allows you to specify the queue name, driver name, port names, processor name, and optionally the share name, comment, and location description. DeletePrintQueue removes a print queue.

PrintSystemJobInfo

The PrintQueue class defines a GetPrintJobInfoCollection method. This returns a collection of PrintSystemJobInfo objects. This collection provides roughly the same information you can see by opening the window showing active print jobs for the printer in Windows.

Some of the information in PrintSystemJobInfo is static for the job lifetime, such as JobName and NumberOfPages. Some properties provide status information; for example, IsPrinting, TimeSinceStartedPrinting, and IsPaperOut. PrintSystemJobInfo also offers a Cancel method to cancel the print job, and Pause and Resume methods to allow a job to be suspended and then resumed.

PrintTicket and PrintCapabilities

When you print from a Windows application, the print dialog provides access to various printing options. These include generic features such as the number of copies to be printed and collation settings. There may also be features common to many printers but not universally supported, such as double-sided printing. In some cases, you may wish to configure features specific to your particular printer type. A PrintTicket object encapsulates all such print settings.

The PrintCapabilities class is related to PrintTicket. Given a PrintQueue, you can call its GetPrintCapabilities method to discover the supported set of features you can add to a PrintTicket for that queue (see Example 15-34). If you call the overload of GetPrintCapabilities that takes no parameters, it will return a PrintCapabilities object with properties indicating which common features are available.

Example 15-34. Examining print capabilities
PrintCapabilities caps = pq.GetPrintCapabilities(  );
foreach (Duplexing duplexType in caps.DuplexingCapability) {
    Console.WriteLine(duplexType);
}

The PrintCapabilities class is convenient. However, it does not provide a complete list of the features the printer has to offer. It defines only the properties for common features. To get the complete set, you need to call the GetPrintCapabilitiesAsXml method. This returns an XML document containing all of the features, including those specific to the model of printer attached to the queue. This document will contain XML elements named Feature for each available feature. Example 15-35 shows one such element.

Example 15-35. Feature from a PrintCapabilities XML document
<psf:Feature name="ns0000:JobEnhancedPCL5Enable">
  <psf:Property name="psf:SelectionType">
    <psf:Value xsi:type="xsd:QName">psk:PickOne</psf:Value>
  </psf:Property>
  <psf:Property name="psk:DisplayName">
    <psf:Value xsi:type="xsd:string">EnhancedPCL5Enable</psf:Value>
  </psf:Property>
  <psf:Option name="ns0000:False" constrained="psk:None">
    <psf:Property name="psk:DisplayName">
      <psf:Value xsi:type="xsd:string">False</psf:Value>
    </psf:Property>
  </psf:Option>
  <psf:Option name="ns0000:True" constrained="psk:None">
    <psf:Property name="psk:DisplayName">
      <psf:Value xsi:type="xsd:string">True</psf:Value>
    </psf:Property>
  </psf:Option>
</psf:Feature>

Example 15-36 uses XPath to extract all of the Feature elements, and to print their DisplayName properties in order to show the names of the features. It then retrieves the Option elements inside each Feature to display the available settings.

Example 15-36. Working with print capabilities in XML form
Stream capsStream = pq.GetPrintCapabilitiesAsXml(  );
XmlDocument capsDoc = new XmlDocument(  );
capsDoc.Load(capsStream);

XmlNamespaceManager nsm = new XmlNamespaceManager(capsDoc.NameTable);
nsm.AddNamespace("psf",
  "http://schemas.microsoft.com/windows/2003/08/printing/printschemaframework");

XmlNodeList features = capsDoc.SelectNodes("//psf:Feature", nsm);
foreach (XmlElement feature in features) {
    XmlNode featureName = feature.SelectSingleNode(
        "psf:Property[@name='psk:DisplayName']/psf:Value/text(  )", nsm);
    Console.WriteLine("Feature: " + featureName.Value);

    XmlNodeList options = feature.SelectNodes("psf:Option", nsm);
    foreach (XmlElement option in options) {
        XmlNode optionName = option.SelectSingleNode(
            "psf:Property[@name='psk:DisplayName']/psf:Value/text(  )", nsm);
        if (optionName != null) {
            Console.WriteLine(" option: " + optionName.Value);
        }
    }
}

Running this against a real printer generates a lot of output—about 40 features, each with a few options. Because that wouldn’t make very interesting reading, Example 15-37 shows just a short excerpt that corresponds to the XML in Example 15-35. It shows a printer-specific feature from one of the authors’ printers, along with the possible settings for that feature.

Example 15-37. Printer-specific feature
Feature: EnhancedPCL5Enable
 option: False
 option: True

If you are interested in only certain features (e.g., you wish to know whether the printer at the end of the PrintQueue supports color and duplex printing), you can build a PrintTicket containing the features you are interested in and pass that to either of the methods for retrieving capabilities. This filters the results, listing only features that are both supported by the target printer and that were in your PrintTicket.

It is sometimes appropriate to specify print settings on a more fine-grained level than an entire print job. For example, if you are printing to a color printer, you might require color only on certain pages, and could indicate to the printer that black-and-white printing should be used on the rest. To exploit this, you would associate a PrintTicket for each FixedPage and FixedDocument, as well as providing one for the whole job. You would set the ticket’s OutputColor property to OutputColor.Color for the color pages, and OutputColor.Grayscale for the other tickets. (For the job-level ticket, either you can let the user provide the settings via a print dialog, or you can pass one in to PrintQueue.CreateXpsDocumentWriter.)

Although print tickets are used to control print settings, it may be appropriate to provide them when writing an XPS file. For example, your application may generate XPS file output that is ultimately destined for a print bureau. In this case, either you can set the ticket on the FixedPage, FixedDocument, and FixedDocumentSequence classes (just as you would for printing) or you can use the package-level API. The writer interfaces for fixed pages, documents, and document sequences offer a settable PrintTicket property.

PrintDialog

As we’ve already seen, if you ask the PrintQueue class to create an XpsDocumentWriter without passing in a specific PrintQueue, it automatically shows the standard print dialog. However, sometimes you will need to exercise more control. For example, you may wish to enable the page range selection functionality, allowing the user to print a specific range of pages from the document. You may also wish to select an initial print queue and print ticket—rather than accepting the current defaults, you might wish to show the same settings the user chose the last time a particular document was printed, for example. Alternatively, although you may be happy with the default print dialog behavior, you might want to examine the PrintQueue or PrintTicket chosen by the user. In these cases, you will need to write code to display the PrintDialog.

Example 15-38 shows how to enable user page range selection in the PrintDialog. This also sets an initial selected range of pages.

Example 15-38. Using PrintDialog
PrintDialog pd = new PrintDialog(  );
pd.MinPage = 1;
pd.MaxPage = 20;
pd.UserPageRangeEnabled = true;
pd.PageRangeSelection = PageRangeSelection.UserPages;
pd.PageRange = new PageRange(4, 8);

if (pd.ShowDialog(  ) == true) {
    PrintQueue pq = pd.PrintQueue;
    PrintTicket pt = pd.PrintTicket;
    ...
}

If the user clicks the Print button, ShowDialog will return true. (The return type is a nullable bool, hence explicit comparison with true in the if statement.) The code goes on to retrieve the print queue and ticket reflecting the user’s chosen print settings. These could then be passed to PrintQueue.CreateXpsDocumentWriter, in order to start printing to the chosen queue with the configured settings.

Media Description

Several classes are available in the System.Printing namespace for working with the size and resolution of the output media. These are described in Table 15-4. The reason for having several different classes to represent “paper size” is that different scenarios require different amounts of information.

Table 15-4. Media description types

Class

Usage

PageMediaSize

Describes the paper size

PageImageableArea

Describes the area of the paper in which printing is possible

PrintDocumentImageableArea

Combines a description of the paper size and the printable area

PageResolution

Describes the horizontal and vertical resolution of the target, and contains a “qualitative” resolution such as Draft or High

Displaying Fixed Documents

The easiest way to display an XPS file is to use the XPS viewer application supplied with the .NET 3.0 Framework. This viewer runs when you double-click on an XPS file. As mentioned earlier, Microsoft also supplies a free viewer for displaying documents on machines without the framework installed. However, it is sometimes useful to be able to display an XPS file within an application. WPF supplies the DocumentViewer control for this. Simply set its Document property to refer to either a FixedDocument or a FixedDocumentSequence, and it will render the document, providing navigation and zoom controls. Example 15-39 shows a simple window containing a DocumentViewer.

Example 15-39. Window with DocumentViewer
<Window x:Class="ShowFixedDocument.Window1"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    Title="Show FixedDocument" Height="300" Width="300">

  <DocumentViewer x:Name="viewer" />

</Window>

Given this XAML, the code-behind file can load an XPS document as shown in Example 15-40.

Example 15-40. Loading an XPS document into a DocumentViewer
string docPath =  Environment.GetCommandLineArgs(  )[1];
XpsDocument doc = new XpsDocument(docPath, FileAccess.Read);
viewer.Document = doc.GetFixedDocumentSequence(  );

This reads the XPS file specified by the first command-line parameter. You could also use a FixedDocument or FixedDocumentSequence built from scratch. Figure 15-8 shows the XPS specification itself loaded into this UI.

DocumentViewer control
Figure 15-8. DocumentViewer control

Where Are We?

The XML Paper Specification is at the heart of printing in WPF. It also acts as a file format for accurately capturing an application’s printable output. WPF lets us work with XPS files at various levels. There are the high-level FixedDocumentSequence, FixedDocument, and FixedPage classes, which mirror the basic structure of an XPS file but allow us to use WPF elements such as layout primitives that are not directly supported in XPS. The XpsDocumentWriter maps between this framework element world and the lower-level world of the XPS file.

We can either build an XPS file on disk or send it directly to the print system. If we provide a “save as XPS” feature, we may choose to add extra structure to enhance the resulting XPS files, typically working directly at the XPS package level with the System.Windows.Xps.Packaging namespace. Or, if we wish, we can go lower still, using the System.IO.Packaging namespace to work directly at the OPC level.

The System.Printing namespace provides us with various types to control the printing process, such as choosing output servers and queues, and configuring the print settings.



[106] * You could always use the PageSetupDialog class provided by Windows Forms. However, be aware that you will pay the usual working set overhead for using Windows Forms features on top of the costs of using WPF. If you are already using Windows Forms in your application (e.g., you are hosting Windows Forms controls via interop, as described in Appendix B), incremental cost of using this dialog will be low. But if it’s the only Windows Forms feature you use, you might want to consider whether it’s worth the impact.

[109] * If you choose to generate an XPS document with the package-level APIs, you will need to do something similar when creating hyperlinks in order to make them usable.

[110] * Fragment IDs are the part of the URL following a # symbol. These are a standard URL feature. WPF uses these for much the same purpose as web pages: to identify a particular target location within a page.

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

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