Generating PDF from a WPF application “for everyone, for nothing, and let no one go offended”

  • Tutorial
A couple of weeks ago, a PDF generation task appeared on the project.
Of course, I, as a developer of WPF UI, immediately opposed the harsh approach of coding the rendering of all PDF primitives in C # code.
And the customer was not opposed to buying a certain paid converter from HTML to PDF, for example.
Everything seems to be simple - we generate a line with HTML markup, using DotLiquid for template generation, and convert it to PDF using one of the many paid converters.
The only ambush is poor HTML compatibility with the page structure of a PDF document.
As soon as I started digging in search of a solution to this problem, as one colleague shared a link to an article with an alternative solution .
I learned from the article that it is possible to generate PDF from an XPS document (this format is supported in WPF FlowDocument).
In addition, a free PDFSharp library was used for generation.

Sources can be downloaded from GitHub .

UPD : this is not the first time I've been observing how an article is being added (the first disadvantages were immediately after publication and are unlikely to relate to the main content), while merging karma. I'm interested in motivation, feedback. Unsubscribe who is dissatisfied with what, if not difficult.


Disclaimer


The source codes presented to your attention do not constitute an example to follow. In order not to drag out the article, I did not follow any design patterns. The source code is a simple "Code Behind" approach. This is also done for ease of perception of the essence, i.e. to focus on PDF generation itself. I think you can easily integrate the main pieces of code into the structure of your project.
Also in the source you will find the massive use of dynamic as a data source for the DotLiquid template. This was also done mainly for simplicity and speed. The DotLiquid website has a description of how to annotate your own classes so that they can be used in the template. Here you, too, can easily adapt my sources to your needs.
Well, it’s also worth mentioning that PDFSharp has detected a problem with FlowDocument / XPS pseudo-fonts. In particular, rendered XPS unrendered list markers are exported to PDF as empty squares. In debug mode, I received Debug.Assert (...) messages with a font import / export error. This problem has not yet been investigated. The problem with lists is easy to get around with a template.

Training


Below is a list of the necessary manipulations:
  • We go to the site about the modified PDFSharp and download from there compiled assemblies or the source code itself. An alternative is PDFSharp versions 1.2 - 1.31, inclusive.
  • Install the DotLiquid library (version 1.7.0 at the time of writing) using NuGet (install Nuget if you have not already done so)
  • Add references to the System.Printing and ReachFramework assemblies to the project in which the PDF will be generated


Main window


Below is the layout of the main window.

Here we see a FlowDocumentReader that will display the rendered FlowDocument. In the markup, you can also see the hardcoded FlowDocument, which I use to create a template using the designer in Visual Studio.
You can also see that I use the usual controls and WPF styles. This is one of the huge bonuses of using FlowDocument to generate PDFs. I can use the controls and style resources of my WPF application. For the approach with HTML as an intermediary, it would be necessary to separately support the assembly of CSS styles and pieces of HTML, which still need to be embedded in the template somehow.

Data context for template


To generate the data context, I added a private method to Code Behind of the main window, in which the creation of DotLiquid.Hash for the dynamic object is hardcoded.
        private DotLiquid.Hash CreateDocumentContext()
        {
            var context = new
            {
                Title = "Hello, Habrahabr!",
                Subtitle = "Experimenting with dotLiquid, FlowDocument and PDFSharp",
                Steps = new List{
                    new { Title = "Document Context", Description = "Create data source for dotLiquid Template"},
                    new { Title = "Rendering", Description = "Load template string and render it into FlowDocument markup with Document Context given"},
                    new { Title = "Parse markup", Description = "Use XAML Parser to prepare FlowDocument instance"},
                    new { Title = "Save to XPS", Description = "Save prepared FlowDocument into XPS format"},
                    new { Title = "Convert XPS to PDF", Description = "Convert XPS to WPF using PDFSharp"},
                }
            };
            return DotLiquid.Hash.FromAnonymousObject(context);
        }

As I wrote in the disclaimer, this is just an example. In a real project you should have some kind of converter for real DTO or ViewModel.
In the developer’s manual on the DotLiquid page, it is written that in the template you cannot just use an instance of some arbitrary class to display a string value. If you write the output of, for example, a DateTime object in the template, then the output of ToString () without parameters will get into the rendered document. But if the object you created, for example some BlaBlaUser , is turned up , then DotLiquid will display an error string instead. And this, by the way, is very good, because You will immediately see the specific place where you made a mistake, while the template will still be rendered.

Template


{{ Title }}
        {{ Subtitle }}
    Steps to generate PDF:
    {% for step in Steps -%}
      
    {% endfor -%}

Keep in mind, instead of inserting the binding to the DotLiquid context directly in the TextBlock.Text attribute, it will be safer to use the nested CDATA block:

This will protect you from characters incompatible with the XML format.

Rendering and parsing FlowDocument


        private void ParseButton_OnClick(object sender, RoutedEventArgs e)
        {
            using (var stream = new FileStream("Templates\\report1.lqd", FileMode.Open))
            {
                using (var reader = new StreamReader(stream))
                {
                    var templateString = reader.ReadToEnd();
                    var template = dotTemplate.Parse(templateString);
                    var docContext = CreateDocumentContext();
                    var docString = template.Render(docContext);
                    DocViewer.Document = (FlowDocument) XamlReader.Parse(docString);
                }
            }
        }

Everything is simple here. Open the file stream with the template, create the template context and render the FlowDocument markup. Using XamlReader, we parse the resulting markup and put the created instance into our FlowDocumentReader. If everything suits us, then we proceed to the conversion of this document to PDF.

PDF Generation


        private void ButtonBase_OnClick(object sender, RoutedEventArgs e)
        {
            using (var stream = new FileStream("doc.xps", FileMode.Create))
            {
                using (var package = Package.Open(stream, FileMode.Create, FileAccess.ReadWrite))
                {
                    using (var xpsDoc = new XpsDocument(package, CompressionOption.Maximum))
                    {
                        var rsm = new XpsSerializationManager(new XpsPackagingPolicy(xpsDoc), false);
                        var paginator = ((IDocumentPaginatorSource)DocViewer.Document).DocumentPaginator;
                        rsm.SaveAsXaml(paginator);
                        rsm.Commit();
                    }
                }
                stream.Position = 0;
                var pdfXpsDoc = PdfSharp.Xps.XpsModel.XpsDocument.Open(stream);
                PdfSharp.Xps.XpsConverter.Convert(pdfXpsDoc, "doc.pdf", 0);
            }
        }

And here everything is simple. A package of an XPS document is generated (as you know, XPS is a zip archive with a lot of XML and other resources). The FlowDocument previously rendered by us is saved in the created XPS package. (Until closing!) The stream of the XPS package is downloading the XPS document using PDFSharp. After that, the downloaded XPS is converted to PDF.

Conclusion


In conclusion, I want to give a list of the advantages that I have identified for myself in this approach.
  • Free - we managed to solve one of the important business problems with the help of free libraries (MIT)
  • FlowDocument as an intermediary - this is almost native support for the page structure and the ability to use WPF controls within the document
  • Styling - thanks to the use of FlowDocument, it is possible to style WPF documents with styles
  • Interactivity - because you can use WPF controls, then before the "printout" in PDF, the user can make some changes and calculations in the document, if necessary. Even the use of Binding is possible in this case (although there are some problems with this - you need a kick for Dispatcher to start updating the Binding).
  • Visual Designer - I can use the usual Visual Studio designer when preparing a template. The only grievance is that DotLiquid binders of the form "{{someProp}}" are incompatible with XAML markup. You can bypass the insert at the beginning of "{}":


THANKS FOR ATTENTION!

Also popular now: