Modifying XPS Document: Add Watermark

Windows Platform Foundation has provided easy APIs and solutions for XPS document generation, visualization and printing. But often time, after XPS documents are generated, we would like to modify it in certain way. The scenario I'm trying to demonstrate here is adding a watermark to each page within an XPS document.

There are multiple ways to modify an XPS document. The lowest level would be working from container and XPS markup directly. But if you're changing font/image resources, it can get quite complicated.

Another way is using WPF to load each page in an XPS document as a Visual, modify the Visual, and then write it back as a new XPS document. We will take the second approach here.

The first step is to create a Visual for watermak:

 Visual CreateWatermark(double width, double height, string message){  DrawingVisual watermark = new DrawingVisual();   using (DrawingContext ctx = watermark.RenderOpen())  {     FormattedText text = new FormattedText(message,       System.Globalization.CultureInfo.CurrentCulture,       FlowDirection.LeftToRight,       new Typeface("Times New Roman"), 96 * 1.5,       new SolidColorBrush(Color.FromScRgb(0.3f, 1f, 0f, 0f))     );      // Rotate transform, keep center     {       double diag = Math.Sqrt(width * width + height * height);       double cX = width / 2;       double cY = height / 2;       double sin = height / diag;       double cos = width / diag;       double dx = (cX * (1.0 - cos)) + (cY * sin);       double dy = (cY * (1.0 - cos)) - (cX * sin);            Transform mat = new MatrixTransform(cos, sin, -sin, cos, dx, dy);        ctx.PushTransform(mat);    }     // Centerlize    double x = (width - text.Width) / 2;    double y = (height - text.Height) / 2;    ctx.DrawText(text, new Point(x, y));    ctx.Pop();  }   return watermark;}

The CreateWatermark routine accepts three parameters, page width, page height, and a text string. It creates a DrawingVisual by drawing to the DrawingContext interface. To make it more like a real watermark, text is rotated according to page dimension and centered.

Once we have code for watermark, we can use XPS-related API to load a document and create a new document:

 void AddWatermark(string filename){  // Open original XPS document  XpsDocument xpsOld = new XpsDocument(filename, FileAccess.Read);  FixedDocumentSequence seqOld = xpsOld.GetFixedDocumentSequence();   // Create new XPS document  Package container = Package.Open("new_" + filename, FileMode.Create);  XpsDocumentWriter writer = XpsDocument.CreateXpsDocumentWriter(new XpsDocument(container));   // Needed for writing multiple pages  SerializerWriterCollator vxpsd = writer.CreateVisualsCollator();   int pageno = 1;   foreach (DocumentReference r in seqOld.References)  {     FixedDocument d = r.GetDocument(false);      // Walk through each page     foreach (PageContent pc in d.Pages)     {        FixedPage fixedPage = pc.GetPageRoot(false);         double width  = fixedPage.Width;        double height = fixedPage.Height;        Size sz = new Size(width, height);         // Convert to WPF Visual        fixedPage.Measure(sz);        fixedPage.Arrange(new Rect(new Point(), sz));        fixedPage.UpdateLayout();         ContainerVisual newpage = new ContainerVisual();        newpage.Children.Add(fixedPage);        newpage.Children.Add(CreateWatermark(width, height, "Confidential (" + pageno + ")"));         pageno ++;         // Write out modified page        vxpsd.Write(newpage);     }  }   vxpsd.EndBatchWrite();   container.Close();  xpsOld.Close();}

There are a few things to notice here:

  • SerializerWriteCollator is used to write multiple Visuals out, each as a FixedPage
  • Measure, Arrange, and UpdateLayout are used to convert FixedPage to a WPF Visual.
  • ContainerVisual is used to combine the original page with the watermark.
  • The contains of the document is re-serialized, so markup and resource may not be the same as in original document.

Finally, here is a sample output: