Printing to legacy devices in Windows Presentation Foundation

In the two previous postings, we discussed about two different ways of generating XPS (XML Paper Specification) Documents, one being printing to XPS Document Writer from Win32 applications, and the other being using the new WPF (Windows Presentation Foundation) API. In this article, we will discuss how to print to printers in WPF.

 

There are two different printing paths under WPF, although the printing API is completely the same. If a printer device supports XPS Document directly, the print sub-system can just generate an XPS Document and sent to the printer; otherwise, the print sub-system will convert WPF contents to GDI commands and send them to GDI/DDI-based printer drivers (also known as legacy devices). The two printing paths approach isolates application from implementation details, encourages printer manufacturers to innovate to provide customer which better printing quality and performance, while at the same time make all existing printers work seamlessly.

 

In the WIN32 world, GDI-based printing API is centered around DEVMODE for printing settings, GeDeviceCapabilities for device capability queries, HPRINTER for printer handler, and HDC for passing graphics primitives to GDI and then to printer driver. In the WPF world, DEVMODE has been replaced by PrintTicket class, GetDeviceCapabilities has been turned to PrinterCapabilities class, HPRINTER morphs into PrintQueue class, and HDC’s role is now played by Visual and UIElement. 

In the XPS Document generation code, we used a very simple Visual. This time, let’s try something little bit more complicated. We would like to generate Visual which reflects the size of the paper it’s going to be printed on, and we want to use more complicated brushes to illustrate internal printing implementation. Here is the Visual generation code we are using:

 

static Visual CreateVisual(PrintTicket ticket)

{

    double width = ticket.PageMediaSize.MediaSizeX;

    double height = ticket.PageMediaSize.MediaSizeY;

    if (ticket.PageOrientation.Value == PrintSchema.OrientationValue.Landscape)

    {

        double t = width; width = height; height = t;

    }

    const double Inch = 96;

    DrawingVisual visual = new DrawingVisual();

    DrawingContext dc = visual.RenderOpen();

    Pen dotPen = new Pen(Brushes.Black, 0.5);

    dotPen.DashStyle = DashStyles.Dot;

    dc.DrawRectangle(null, dotPen, new Rect(Inch / 4, Inch / 4, width - Inch / 2, height - Inch / 2));

    LinearGradientBrush vertical = new LinearGradientBrush(Colors.Yellow, Colors.Blue, 90);

    dc.DrawRectangle(vertical, null, new Rect(Inch / 2, Inch / 2, Inch * 1.5, Inch * 1.5));

    LinearGradientBrush horizontal = new LinearGradientBrush(Colors.Red, Colors.Green, 0);

    horizontal.Opacity = 0.6;

    dc.DrawEllipse(horizontal, null, new Point(Inch * 2.25, Inch * 2), Inch * 1.25, Inch);

    dc.Close();

   

    return visual;

}

Here is what the code is doing:

 

  • It queries current paper size, and switches width and height if it’s printing landscape mode. So now width and height define the drawing area.

  • It then draws a dotted rectangle ¼ inch from all four sides. Notice that in WPF, point (0, 0) is aligned to the top left corner of the paper, not the top-left corner of the printable area as in GDI.

  • The rectangle following it uses a vertical linear gradient brush.

  • The last graphics primitive is an ellipse with a 60% transparent horizontal linear gradient brush. 

     

To print a Visual, first we need to get a PrintQueue (replacement for HPRINT). The code to get the default printer queue and default PrintTicket (replacement for DEVMODE) is quite simple:

static PrintQueue GetDefaultPrintQueue(out PrintTicket ticket)

{

    LocalPrintServer server = new LocalPrintServer();

    PrintQueue queue = server.DefaultPrintQueue;

    ticket = queue.DefaultPrintTicket;

    return queue;

}

 

Once a Visual and a PrintQueue is generated, printing the Visual as a single page is very easy:

PrintTicket ticket;

PrintQueue queue = GetDefaultPrintQueue(out ticket);

XpsDocumentWriter writer = PrintQueue.CreateXpsDocumentWriter(queue);

writer.Write(CreateVisual(ticket), ticket); 

With a few more lines of code, we can print multiple pages:

PrintTicket ticket;

PrintQueue queue = GetDefaultPrintQueue(out ticket);

XpsDocumentWriter writer = PrintQueue.CreateXpsDocumentWriter(queue);

VisualsToXpsDocument vtoxpsd = writer.CreateVisualsCollator();

vtoxpsd.Write(CreateVisual(ticket), ticket);

ticket.PageOrientation.Value = PrintSchema.OrientationValue.Landscape;

vtoxpsd.Write(CreateVisual(ticket), ticket);

vtoxpsd.EndBatchWrite();

The code above prints the first page in default orientation (normally portrait) and then switches to landscape mode for the second page. Being a developer in the Windows printing system, I normally prefer paperless printer for my endless printing needs. One of my favorite is Microsoft Office Document Image Writer. It will display the two pages as:

Notice that the dotted rectangles are somehow not displayed perfectly in small scale.

The most interesting thing in this test case is the transparency handling on a legacy device like Microsoft Office Document Image Writer. We mentioned before that for legacy devices, we will convert WPF contents into GDI representation. One of the most difficulty conversions is removing, implementing or simulating transparency. GDI+, which is used by WinForm and Microsoft Office applications, has the same issue for printing. GDI+ solves the issue using by using GDI raster operations and Postscript image masks to approximate transparency, both of them has fidelity problem. Now let’s zoom in to exam WPF’s implementation of transparency on legacy printers. 

 

Transparency is almost as good as WPF rendering of the Visual on screen. The main difference you see is WPF’s anti-aliasing rendering vs. GDI’s aliasing rendering. What WPF legacy printing path code does is removing transparency in vector space as much as possible.