DIY generation of XPS Documents
Microsoft provides two ways of generating XPS Documents. If you're a Win32 application or a .NET 1.0 application, you can generate XPS Documents using the Microsoft XPS Document Writer through Win32 GDI printing API. If you're a WPF (Windows Presentation Foundation) application, generation of XPS Document is supported through our new API. Both cases have been covered in this blog.
But we're still hearing people asking about how to generate XPS Documents outside of WPF and without using XPS Document Writer. The short answer is that XPS is designed to be a simple document format, so it should be quite easy to generate XPS Document on your own.
The longer answer is check the code below. This is what I've got today while working full time as a PDC hands-on-lab proctor:
#include "stdafx.h"
#include <stdio.h>
class CXpsGenerator
{
HDC m_hDC;
int m_dpi;
HGDIOBJ m_pen;
HGDIOBJ m_brush;
public:
CStringA m_xps;
int m_fillMode;
int m_penAlpha;
int m_brushAlpha;
CXpsGenerator()
{
m_pen = GetStockObject(BLACK_PEN);
m_brush = GetStockObject(WHITE_BRUSH);
m_fillMode = 0;
m_penAlpha = 255;
m_brushAlpha = 255;
}
void SelectPen(HGDIOBJ pen)
{
m_pen = pen;
}
void SelectBrush(HGDIOBJ brush)
{
m_brush = brush;
}
void WritePen()
{
if (m_pen == NULL)
{
return;
}
LOGPEN lp;
GetObject(m_pen, sizeof(lp), & lp);
int width = lp.lopnWidth.x;
if (width <= 0)
{
width = 1;
}
m_xps.AppendFormat("StrokeThickness=\"%d\" ", width);
m_xps.AppendFormat("Stroke=\"#%02X%02X%02X%02X\" ", m_penAlpha,
GetRValue(lp.lopnColor), GetGValue(lp.lopnColor), GetBValue(lp.lopnColor));
// pen style and EXTPEN missing
}
void WriteBrush()
{
if (m_brush == NULL)
{
return;
}
LOGBRUSH lb;
GetObject(m_brush, sizeof(lb), & lb);
switch (lb.lbStyle)
{
case PS_SOLID:
m_xps.AppendFormat("Fill=\"#%02X%02X%02X%02X\" ", m_brushAlpha,
GetRValue(lb.lbColor), GetGValue(lb.lbColor), GetBValue(lb.lbColor));
break;
// other brush style missing
}
}
void WritePath()
{
::EndPath(m_hDC);
int n = ::GetPath(m_hDC, NULL, NULL, 0);
if (n <= 0)
{
return;
}
int m = (n + 3 ) / 4 * 4;
BYTE * pType = new BYTE[m * (1 + sizeof(POINT))];
POINT * pPoint = (POINT *) (pType + m);
GetPath(m_hDC, pPoint, pType, n);
m_xps.Append("<Path ");
WritePen();
WriteBrush();
m_xps.AppendFormat("Data=\"F%d", m_fillMode);
for (int i = 0; i < n; i ++)
{
switch (pType[i] & ~ PT_CLOSEFIGURE)
{
case PT_MOVETO:
m_xps.Append(" M");
break;
case PT_LINETO:
m_xps.Append(" L");
break;
case PT_BEZIERTO:
m_xps.Append(" C");
pType[i + 1] |= 32; // avoid generating C to two subsequent points
pType[i + 2] |= 32;
break;
}
m_xps.AppendFormat(" %d,%d", pPoint[i].x, pPoint[i].y);
if (pType[i] & PT_CLOSEFIGURE)
{
m_xps.Append("z");
}
}
m_xps.Append("\" />\r\n");
delete [] pType;
}
void Rectangle(int left, int top, int right, int bottom)
{
BeginPath(m_hDC);
::Rectangle(m_hDC, left, top, right, bottom);
WritePath();
}
void Ellipse(int left, int top, int right, int bottom)
{
BeginPath(m_hDC);
::Ellipse(m_hDC, left, top, right, bottom);
WritePath();
}
void StartPage(double width, double height, HDC hDC)
{
m_hDC = hDC;
m_dpi = GetDeviceCaps(hDC, LOGPIXELSX);
m_dpi = 300;
m_xps.Append("<FixedPage xmlns=\"https://schemas.microsoft.com/xps/2005/06\" ");
m_xps.Append("xmlns:x=\"https://schemas.microsoft.com/xps/2005/06/resourcedictionary-key\" xml:lang=\"en-us\" ");
m_xps.AppendFormat("Width=\"%f\" Height=\"%f\" >\r\n", width * 96, height * 96);
if (m_dpi != 96) // scale to 96 dpi
{
m_xps.AppendFormat("<Canvas RenderTransform=\"matrix(%f, 0, 0, %f, 0, 0)\" >\r\n",
96.0 / m_dpi, 96.0 / m_dpi);
}
}
void EndPage()
{
if (m_dpi != 96)
{
m_xps.Append("</Canvas>\r\n");
}
m_xps += "</FixedPage>\r\n";
}
};
int _tmain(int argc, _TCHAR* argv[])
{
CString s;
CXpsGenerator xps;
HDC hDC = GetDC(NULL);
xps.StartPage(8.5, 11, hDC);
HBRUSH yellowBrush = CreateSolidBrush(RGB(0xFF, 0xFF, 0));
HPEN bluePen = CreatePen(PS_SOLID, 1, RGB(0, 0, 0xFF));
xps.SelectBrush(yellowBrush);
xps.SelectPen(bluePen);
xps.Rectangle(48, 48, 192, 192);
xps.SelectPen(GetStockObject(BLACK_PEN));
HBRUSH cyanBrush = CreateSolidBrush(RGB(0xFF, 0, 0xFF));
xps.SelectBrush(cyanBrush);
xps.m_brushAlpha = 128;
xps.Ellipse(120, 120, 120 + 192, 120 + 128);
xps.m_brushAlpha = 255;
xps.EndPage();
DeleteObject(yellowBrush);
DeleteObject(bluePen);
DeleteObject(cyanBrush);
ReleaseDC(NULL, hDC);
puts(xps.m_xps);
}
The code should be quite easy to read. The CXpsGenerator class supports generating XML markup for simple pages with vector graphics using simple brushes and pens. It's interface is similar to GDI. More methods can be added to support more brushes, more pens, more vector primitives, images and texts. To generate the full XPS container, you also need a way to generate .zip container and create other streams which make a XPS document complete.
Here is the markup generated:
<FixedPage xmlns="https://schemas.microsoft.com/xps/2005/06"
xmlns:x="https://schemas.microsoft.com/xps/2005/06/resourcedictionary-key"
xml:lang="en-us" Width="816.000000" Height="1056.000000" >
<Path StrokeThickness="1" Stroke="#FF0000FF" Fill="#FFFFFF00"
Data="F0 M 191,48 L 48,48 L 48,191 L 191,191z" />
<Path StrokeThickness="1" Stroke="#FF000000" Fill="#80FF00FF"
Data="F0 M 311,184 C 311,148 268,120 216,120 C 163,120 120,148 120,184 C 120,219 163,247 216,247 C 268,247 311,219 311,184" />
</FixedPage>
It's definitely more costly to generate XPS Documents on your own, comparing with using the XPS Document Writter. But there are a few possible benefits:
- Less dependency.
- More efficient, because you're bypassing GDI, DDI and spooler.
- Making use of more XPS features, which may not be accessible through GDI:
- Transparency: XPS Document Writer can understand a few transparency simulation pattens.
- Brushes: more complicated linear gradient brushes, radial gradient brushes, visual brushes, more tiling modes, viewbox.
- Pen: more pen styles.
- Opacity mask.
- More compressed image file format support.
- Resource dictionary.
- Extra meta data.