📜 ⬆️ ⬇️

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

A couple of weeks ago on the project appeared the task of generating PDF.
Of course, I, as a developer of WPF UI, was immediately against the harsh approach of coding the rendering of all PDF primitives in C # code.
And the customer was not opposed to buying some paid converter from HTML to PDF, for example.
It seems to be simple - we generate a string with HTML markup using DotLiquid for templating, and convert it to PDF using one of the many paid converters.
The only ambush is the poor HTML compatibility with the page structure of the PDF document.
Only I began to dig in the search for solutions 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 a PDF from an XPS document (this format is supported in a WPF FlowDocument).
In addition, the free PDFSharp library was used for generation.

Sources can be downloaded from GitHub .

UPD : it ’s not the first time I’m watching how an article is being added (the first cons were immediately after publication and hardly belong to the main content), at the same time merging karma. I am interested in motivation, feedback. Decide who is dissatisfied with what, if not difficult.

')

Disclaimer


The source codes presented to you do not constitute an example to follow. In order not to delay 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 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 a template. Here you can easily adapt my sources to your needs.
Well, it’s still worth mentioning that PDFSharp had a problem with FlowDocument / XPS pseudo-fonts. In particular, rendered markers of an unnumbered list from XPS are exported to PDF in the form of empty squares. In debug mode, I received messages Debug.Assert (...) with an error importing / exporting fonts. This problem has not yet been investigated. The problem with lists is easy to get around using a template.

Training


Below is a list of the necessary manipulations:


Main window


Below is the layout of the main window.
<Window x:Class="Solution.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="MainWindow" Height="480" Width="640"> <Grid> <Grid.RowDefinitions> <RowDefinition></RowDefinition> <RowDefinition Height="Auto"></RowDefinition> </Grid.RowDefinitions> <FlowDocumentReader x:Name="DocViewer"> <FlowDocument> <FlowDocument.Resources> <Style TargetType="TextBlock"> <Setter Property="FontSize" Value="14"/> <Setter Property="Margin" Value="5"/> </Style> </FlowDocument.Resources> <BlockUIContainer> <Grid> <Grid.ColumnDefinitions> <ColumnDefinition Width="Auto"/> <ColumnDefinition Width="Auto"/> <ColumnDefinition /> </Grid.ColumnDefinitions> <Ellipse Fill="#003481" Width="5" Height="5" Margin="5"/> <TextBlock Text="Title" FontWeight="Bold" Grid.Column="1"/> <TextBlock Text="Description" Grid.Column="2"/> </Grid> </BlockUIContainer> </FlowDocument> </FlowDocumentReader> <StackPanel Grid.Row="1" Orientation="Horizontal"> <Button Click="ParseButton_OnClick">Parse</Button> <Button Click="ButtonBase_OnClick">Print</Button> </StackPanel> </Grid> </Window> 

Here we see the FlowDocumentReader , which will display the rendered FlowDocument. In the markup, you can also see the hard-coded FlowDocument, which I use to create a template using the designer in Visual Studio.
You can also see that I use the usual WPF controls and styles. This is one of the great benefits of using FlowDocument to generate PDF. I can use the controls and style resources of my WPF application. For an approach with HTML as an intermediary, we would have to separately support the assembly of CSS styles and chunks of HTML, which still need to be embedded in the template.

Data context for the 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 a dynamic object is hard-coded.
  private DotLiquid.Hash CreateDocumentContext() { var context = new { Title = "Hello, Habrahabr!", Subtitle = "Experimenting with dotLiquid, FlowDocument and PDFSharp", Steps = new List<dynamic>{ 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 a disclaimer, this is just an example. In a real project, you should have some kind of converter for real DTO or ViewModel.
The developer’s manual on the DotLiquid page says that the template cannot just use an instance of an arbitrary class to display a string value. If you specify the output of, for example, a DateTime object in a template, then the output of ToString () without parameters will be included in the rendered document. But if an object created by you, for example, some BlaBlaUser , turns up , then DotLiquid will print a line with an error instead. And this, by the way, is very good, because You will immediately see the specific place where you made a mistake, but the template will still be rendered.

Template


 <FlowDocument xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"> <FlowDocument.Resources> <Style TargetType="TextBlock"> <Setter Property="FontSize" Value="14"/> <Setter Property="Margin" Value="5"/> <Setter Property="TextWrapping" Value="Wrap"/> </Style> </FlowDocument.Resources> <Paragraph FontSize="24"> <Bold>{{ Title }}</Bold> </Paragraph> <Paragraph FontSize="16"> {{ Subtitle }} </Paragraph> <Paragraph FontSize="16"> <Bold>Steps to generate PDF:</Bold> </Paragraph> {% for step in Steps -%} <BlockUIContainer> <Grid> <Grid.ColumnDefinitions> <ColumnDefinition Width="Auto"/> <ColumnDefinition Width="Auto"/> <ColumnDefinition /> </Grid.ColumnDefinitions> <Ellipse Fill="#003481" Width="5" Height="5" Margin="5"/> <TextBlock Text="{{ step.Title }}" Foreground="#003481" FontWeight="Bold" Grid.Column="1"/> <TextBlock Text="{{ step.Description }}" Grid.Column="2"/> </Grid> </BlockUIContainer> {% endfor -%} </FlowDocument> 

Keep in mind, instead of inserting a binding to the DotLiquid context directly in the TextBlock.Text attribute, it is safer to use the nested CDATA block:
 <TextBlock Foreground="#003481" FontWeight="Bold" Grid.Column="1"> <![CDATA[ {{ step.Title }} ]]> </TextBlock> 

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); } } } 

It's simple. Open the file stream with the template, create a template context and render the FlowDocument markup. Using the XamlReader, parse the resulting markup and put the created instance into our FlowDocumentReader. If everything suits us, then proceed to convert 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. An XPS document package is generated (as you know, XPS is a zip archive with many XML and other resources). The previously rendered FlowDocument is saved to the created XPS package. (Before closing!) The XPS package stream downloads the XPS document using PDFSharp tools. After that, the downloaded XPS is converted to PDF.

Conclusion


In conclusion, I would like to give a list of the benefits that I highlighted for myself in this approach.


THANKS FOR ATTENTION!

Source: https://habr.com/ru/post/201836/


All Articles