Introduction
Often we need to print the screen or some parts of the screen. This is pretty useful for printing graphic reports, like charts. In this article, we’re going to have a look at how to print a visual element with the WPF Visual Print. The essential part is serializing an XAML element to an XPS document, and converting the XPS document to a FlowDocument
. Then we print and print preview the FlowDocument
with the FlowDocument
Viewer.
What is an XPS Document
The XML Paper Specification (XPS) format is basically an electronic representation of digital documents based on XML. It is a paginated fixed-layout format that retains the look and feel of your electronic documents. XPS documents can be easily created once you have the right software installed, like Microsoft Word.
The parts of an XPS document are organized in a logical hierarchy with the FixedDocumentSequence
part at the top. An XPS document package may contain more than one document, and the sequence of these documents is described by the FixedDocumentSequence
part. The FixedDocumentSequence
part references the FixedDocument
parts that, in turn, reference the pages of each document within the package.
Each FixedDocument
part references the pages of that document as FixedPage
parts. Each FixedPage
part contains the text markup and layout of a page in the document as well as references to images, fonts, and other custom resources used in the page. Resources such as images and fonts are stored in the package but outside of the FixedPage
part, allowing them to be shared by other pages. This is especially useful for font resources, but it could also be useful for any image resource that is used on more than one page, such as a watermark or letterhead logo.
I know it’s pretty boring for you to read these definitions. But I have to bring them out, because all these definitions will be used in the WPF Visual Print code.
Serialize a Visual Component to XPS Document
XPS documents are stored in a file, called a package, that conforms to the Open Packaging Conventions and are composed of a set of document components known as parts. A package has a physical and a logical organization. The physical organization consists of the document parts and folders inside the package, and the logical organization is a hierarchy described by the document parts. The XML Paper Specification applies a specific organization and naming convention to the logical layer for XPS documents.
WPF wraps the XPS API in the XPSSerializationManager
class. Thus WPF can create an XPS document by serializing the XAML element page to an XPS document. Here is our serialization code:
FrameworkElement fe = (visual as FrameworkElement);
fe.Measure(new Size(Int32.MaxValue, Int32.MaxValue));
Size visualSize = fe.DesiredSize;
fe.Arrange(new Rect(new Point(0, 0), visualSize));
MemoryStream stream = new MemoryStream();
string pack = "pack://temp.xps";
Uri uri = new Uri(pack);
DocumentPaginator paginator;
XpsDocument xpsDoc;
using (Package container = Package.Open(stream, FileMode.Create))
{
PackageStore.AddPackage(uri, container);
using (xpsDoc = new XpsDocument(container, CompressionOption.Fast, pack))
{
XpsSerializationManager rsm =
new XpsSerializationManager(new XpsPackagingPolicy(xpsDoc), false);
rsm.SaveAsXaml(visual);
paginator = ((IDocumentPaginatorSource)
xpsDoc.GetFixedDocumentSequence()).DocumentPaginator;
paginator.PageSize = visualSize;
}
PackageStore.RemovePackage(uri);
}
Custom Document
When you serialize an XAML element to an XPS document, it’s always one page. Apparently, it’s not good enough for visual elements that need multiple pages. So you need to write your own paginator class, VisualDocumentPaginator
.
The VisualDocumentPaginator
constructor modifies the page size of the original paginator based on the required page size and margin. The new GetPage
method calls the original GetPage
method to get a page, then tries to measure the visual element size and split it to multiple pages per page size. Here is our VisualDocumentPaginator
class:
public class VisualDocumentPaginator : DocumentPaginator
{
Size m_PageSize;
Size m_Margin;
DocumentPaginator m_Paginator = null;
int m_PageCount;
Size m_ContentSize;
ContainerVisual m_PageContent;
ContainerVisual m_SmallerPage;
ContainerVisual m_SmallerPageContainer;
ContainerVisual m_NewPage;
public VisualDocumentPaginator(DocumentPaginator paginator,
Size pageSize, Size margin)
{
m_PageSize = pageSize;
m_Margin = margin;
m_Paginator = paginator;
m_ContentSize = new Size(pageSize.Width - 2 * margin.Width,
pageSize.Height - 2 * margin.Height);
m_PageCount = (int)Math.Ceiling(m_Paginator.PageSize.Height /
m_ContentSize.Height);
m_Paginator.PageSize = m_ContentSize;
m_PageContent = new ContainerVisual();
m_SmallerPage = new ContainerVisual();
m_NewPage = new ContainerVisual();
m_SmallerPageContainer = new ContainerVisual();
}
Rect Move(Rect rect)
{
if (rect.IsEmpty)
{
return rect;
}
else
{
return new Rect(rect.Left + m_Margin.Width,
rect.Top + m_Margin.Height,
rect.Width, rect.Height);
}
}
public override DocumentPage GetPage(int pageNumber)
{
m_PageContent.Children.Clear();
m_SmallerPage.Children.Clear();
m_NewPage.Children.Clear();
m_SmallerPageContainer.Children.Clear();
DrawingVisual title = new DrawingVisual();
using (DrawingContext ctx = title.RenderOpen())
{
FontFamily font = new FontFamily("Times New Roman");
Typeface typeface =
new Typeface(font, FontStyles.Normal,
FontWeights.Bold, FontStretches.Normal);
FormattedText text = new FormattedText("Page " +
(pageNumber + 1) + " of " + m_PageCount,
System.Globalization.CultureInfo.CurrentCulture,
FlowDirection.LeftToRight,
typeface, 14, Brushes.Black);
ctx.DrawText(text, new Point(0, 0));
}
DocumentPage page = m_Paginator.GetPage(0);
m_PageContent.Children.Add(page.Visual);
RectangleGeometry clip = new RectangleGeometry(
new Rect(0, m_ContentSize.Height * pageNumber,
m_ContentSize.Width, m_ContentSize.Height));
m_PageContent.Clip = clip;
m_PageContent.Transform =
new TranslateTransform(0, -m_ContentSize.Height * pageNumber);
m_SmallerPage.Children.Add(m_PageContent);
m_SmallerPage.Transform = new ScaleTransform(0.95,0.95);
m_SmallerPageContainer.Children.Add(m_SmallerPage);
m_SmallerPageContainer.Transform = new TranslateTransform(0, 24);
m_NewPage.Children.Add(title);
m_NewPage.Children.Add(m_SmallerPageContainer);
m_NewPage.Transform =
new TranslateTransform(m_Margin.Width, m_Margin.Height);
return new DocumentPage(m_NewPage, m_PageSize,
Move(page.BleedBox),Move(page.ContentBox));
}
public override bool IsPageCountValid
{
get
{
return true;
}
}
public override int PageCount>
{
get
{
return m_PageCount;
}
}
public override Size PageSize
{
get
{
return m_Paginator.PageSize;
}
set
{
m_Paginator.PageSize = value;
}
}
public override IDocumentPaginatorSource Source
{
get
{
if (m_Paginator != null)
return m_Paginator.Source;
return null;
}
}
}
Here is the code which converts the default XPS document to a multiple pages XPS document:
using (Package container = Package.Open(stream, FileMode.Create))
{
using (xpsDoc = new XpsDocument(container, CompressionOption.Fast, pack))
{
paginator = new VisualDocumentPaginator(paginator,
new Size(pageSize.Width, pageSize.Height),
new Size(48, 48));
XpsSerializationManager rsm = new XpsSerializationManager(
new XpsPackagingPolicy(xpsDoc), false);
rsm.SaveAsXaml(paginator);
}
PackageStore.RemovePackage(uri);
}
Print and Print Preview with FlowDocument
Why We Use FlowDocument
A flow document is designed to "reflow content" depending on the window size, device resolution, and other environment variables. In addition, flow documents have a number of built-in features including search, viewing modes that optimize readability, and the ability to change the size and appearance of fonts. Flow documents are best utilized when ease of reading is the primary document consumption scenario.
Convert an XPS Document to a FlowDocument
There is an excellent article that talks about how to convert an XPS document to a flow document. I won’t repeat it here. As we talked before, an XPS document consists of multiple fixed pages. Every fixed page contains UI Element
children. So in short words, this conversion extracts the UI Element
children of a FixedPage
and adds them to a FlowDocument
UI block.
One thing I need to bring out is, the font resource in an XPS document is obfuscated. These fonts normally are saved as ODTTF files. Before converting to a flowdocument
, we need to de-obfuscate first. In .NET Framework 3.5, we still use the same name for the de-obfuscated font file to generate a Glyph. But in .NET Framework 4.0, you’ll get a null
reference exception when you try to generate a Glyph with an ODTTF file, even this file is de-obfuscated. That’s null
reference exception from MS.Internal.FontCache.FontFaceLayoutInfo.IntMap.TryGetValue( Int32 key, UInt16& value)
”. So you have to rename the de-obfuscated font file to a TTF file, and save font file to different folder. You can name the folder as font name.
Shown below is the code to de-obfuscate an ODTTF font. You can get the GUID from the ODTT font file name:
private static void DeobfuscateData(byte[] fontData, string guid)
{
byte[] guidBytes = new byte[16];
for (int i = 0; i < guidBytes.Length; i++)
{
guidBytes[i] = Convert.ToByte(guid.Substring(i * 2, 2), 16);
}
for (int i = 0; i < 32; i++)
{
int gi = guidBytes.Length - (i % guidBytes.Length) - 1;
fontData[i] ^= guidBytes[gi];
}
}
Print FlowDocument
Call the PrintDialog.PrintDocument
method to call the print dialog that will allow you to select a printer and send a document to the printer to print it. The PrintDocument
method of PrintDialog
takes a DocumentPaginator
object that you can get from the IDocumentPaginatorSource.DocumentPaginator
property as listed in the following code:
DocumentPaginator paginator =
((IDocumentPaginatorSource)m_FlowDocument).DocumentPaginator;
m_PrintDialog.PrintDocument(paginator, "Printing");