6

I have a Canvas element on a XAML page that I'm using as a printing root in my UWP app. I'm using events such as PrintManager.PrintTaskRequested and PrintDocument.Paginate to prepare my report and send it to a printer.

I need to export the report programmatically to a PDF file. The ideal solution would somehow utilize the existing printing root (the Canvas), print to it, and then convert the result to PDF. After that, I would be able to save PDF to a file or attach it to an email.

I've been searching for a suitable solution for a while but nothing works in UWP. For example, this post provides a perfect solution it does not seem to work in UWP:

How to programmatically print to PDF file without prompting for filename in C# using the Microsoft Print To PDF printer that comes with Windows 10

I would appreciate any help.

ata6502
  • 139
  • 1
  • 10
  • Print Preview dialog should have save to PDF under the dropdown where you select printer. Will that option not suffice for you requirements? – AVK Jun 22 '17 at 00:28
  • Thanks AVK for your suggestion. No, the Save to PDF option in the Print Preview dialog is not sufficient. I need to perform all the conversion from XAML Canvas to PDF **programmatically** without involving the Print Preview dialog. The conversion has to be done behind the scene without the user interaction. – ata6502 Jun 22 '17 at 02:05
  • I think you can change the Page to Image. – lindexi Jun 22 '17 at 12:23
  • Thanks lindexi for your response. I have already tried to convert the XAML Canvas to an image but the quality of the output was very poor. Besides, I need a PDF document rather than one big image. – ata6502 Jun 22 '17 at 18:08
  • You should check [this](http://www.uwpcommunitytoolkit.com/en/master/helpers/PrintHelper/) – AVK Jun 23 '17 at 06:12
  • Hi AVK, could you explain how the PrintHelper could help me with converting XAML elements to a PDF file, please? I think I'm missing something. – ata6502 Jun 26 '17 at 14:14

1 Answers1

7

I struggled with this issue for a long time - automatic, programatic XAML to PDF conversion in UWP - and finally figured out an excellent solution.

There are several libraries for creating PDFs programatically in UWP. The trick is the XAML conversion. I took the following approach:

A) Traverse the XAML tree and generate a list of controls to be converted. In my case, textblocks and borders, but this could be expanded.

B) Declare a PDF page size matching your XAML's actual size.

C) Go through the list, get the control coordinates. Use the appropriate function in C1PDF to create the same element in the PDF. This code also checks for any RotateTransforms and applies the angle of rotation to the text as well.

Using this solution, I was able to produce an exact likeness of the XAML UI - which itself represented a print document - as a PDF, fully scalable and with perfect print rendering.

Here's some code to get you on your way, using the ComponentOne PDF controls, that I wrote to convert my XAML control to PDF:

Async Function XAMLtoPDF(myXAMLcontrol As Control) As Task(Of Boolean)
    Dim pdf As C1PdfDocument
    pdf = New C1PdfDocument(PaperKind.Letter)
    Dim lTB As New List(Of Object)

    pdf.PageSize = New Size(myXAMLcontrol.ActualWidth, myXAMLcontrol.ActualHeight)

    FindTextBlocks(myXAMLcontrol, lTB)
    For x = 0 To lTB.Count - 1
        If TypeOf lTB(x) Is TextBlock Then
            Dim TB As TextBlock = lTB(x)
            Dim obj As FrameworkElement = TB
            Dim angle As Double = 0
            Do While obj IsNot Nothing
                Dim renderxform As Transform = obj.RenderTransform
                If TypeOf renderxform Is TransformGroup Then
                    Dim tg As TransformGroup = CType(renderxform, TransformGroup)
                    For Each t As Transform In tg.Children
                        If TypeOf t Is RotateTransform Then
                            angle -= CType(t, RotateTransform).Angle
                        End If
                    Next
                ElseIf TypeOf renderxform Is RotateTransform Then
                    angle -= CType(renderxform, RotateTransform).Angle
                End If
                obj = obj.Parent
            Loop

            Dim myfont As Font
            Select Case TB.FontStyle
                Case FontStyle.Normal
                    If TB.FontWeight.Weight = FontWeights.Bold.Weight Then
                        myfont = New Font(TB.FontFamily.Source, TB.FontSize, PdfFontStyle.Bold)
                    Else
                        myfont = New Font(TB.FontFamily.Source, TB.FontSize, PdfFontStyle.Regular)
                    End If
                Case Else  'FontStyle.Oblique, FontStyle.Italic             '
                    myfont = New Font(TB.FontFamily.Source, TB.FontSize, PdfFontStyle.Italic)
            End Select

            Dim ttv As GeneralTransform = TB.TransformToVisual(myXAMLcontrol)
            Dim ScreenCoords As Point = ttv.TransformPoint(New Point(0, 0))
            Dim myWidth As Double, myHeight As Double
            If TB.TextWrapping = TextWrapping.NoWrap Then
                myWidth = pdf.MeasureString(TB.Text, myfont).Width
                myHeight = pdf.MeasureString(TB.Text, myfont).Height
            Else
                myWidth = TB.ActualWidth + 10       'Admittedly, 10 is a kluge factor to make wrapping match'
                myHeight = pdf.MeasureString(TB.Text, myfont, myWidth).Height
            End If
            Dim rc As New Rect(ScreenCoords.X, ScreenCoords.Y, myWidth, myHeight)

            If angle Then
                Dim fmt As New StringFormat()
                fmt.Angle = angle
                pdf.DrawString(TB.Text, myfont, CType(TB.Foreground, SolidColorBrush).Color, rc, fmt)
            Else
                pdf.DrawString(TB.Text, myfont, CType(TB.Foreground, SolidColorBrush).Color, rc)
            End If
        ElseIf TypeOf lTB(x) Is Border Then
            Dim BDR As Border = lTB(x)
            Dim ttv As GeneralTransform = BDR.TransformToVisual(myXAMLcontrol)
            Dim ScreenCoords As Point = ttv.TransformPoint(New Point(0, 0))
            Dim pts() As Point = {
                New Point(ScreenCoords.X, ScreenCoords.Y),
                New Point(ScreenCoords.X + BDR.ActualWidth, ScreenCoords.Y),
                New Point(ScreenCoords.X + BDR.ActualWidth, ScreenCoords.Y + BDR.ActualHeight),
                New Point(ScreenCoords.X, ScreenCoords.Y + BDR.ActualHeight)}

            Dim Clr As Color = CType(BDR.BorderBrush, SolidColorBrush).Color
            If BDR.BorderThickness.Top Then pdf.DrawLine(New Pen(Clr, BDR.BorderThickness.Top), pts(0), pts(1))
            If BDR.BorderThickness.Right Then pdf.DrawLine(New Pen(Clr, BDR.BorderThickness.Right), pts(1), pts(2))
            If BDR.BorderThickness.Bottom Then pdf.DrawLine(New Pen(Clr, BDR.BorderThickness.Bottom), pts(2), pts(3))
            If BDR.BorderThickness.Left Then pdf.DrawLine(New Pen(Clr, BDR.BorderThickness.Left), pts(3), pts(0))
        ElseIf TypeOf lTB(x) Is Rectangle Then
            Dim Rect As Rectangle = lTB(x)
            Dim ttv As GeneralTransform = Rect.TransformToVisual(myXAMLcontrol)
            Dim ScreenCoords As Point = ttv.TransformPoint(New Point(0, 0))
            Dim pts() As Point = {
                New Point(ScreenCoords.X + Rect.Margin.Left, ScreenCoords.Y + Rect.Margin.Top),
                New Point(ScreenCoords.X + Rect.ActualWidth - Rect.Margin.Right, ScreenCoords.Y + Rect.Margin.Top),
                New Point(ScreenCoords.X + Rect.ActualWidth - Rect.Margin.Right, ScreenCoords.Y + Rect.ActualHeight - Rect.Margin.Bottom),
                New Point(ScreenCoords.X + Rect.Margin.Left, ScreenCoords.Y + Rect.ActualHeight - Rect.Margin.Bottom)}

            Dim MyPen1 As New Pen(CType(Rect.Stroke, SolidColorBrush).Color, Rect.StrokeThickness)
            MyPen1.DashStyle = DashStyle.Custom
            MyPen1.DashPattern = Rect.StrokeDashArray.ToArray
            Dim MyPen2 As New Pen(CType(Rect.Stroke, SolidColorBrush).Color, Rect.StrokeThickness)
            MyPen2.DashStyle = DashStyle.Custom
            MyPen2.DashPattern = Rect.StrokeDashArray.ToArray

            pdf.DrawLine(MyPen2, pts(0), pts(1))
            pdf.DrawLine(MyPen1, pts(1), pts(2))
            pdf.DrawLine(MyPen2, pts(2), pts(3))
            pdf.DrawLine(MyPen1, pts(3), pts(0))
        End If
    Next
    Dim file As StorageFile = Await ThisApp.AppStorageFolder.CreateFileAsync("Temp.PDF", Windows.Storage.CreationCollisionOption.ReplaceExisting)
    Await pdf.SaveAsync(file)
    Return True
End Function

Private Sub FindTextBlocks(uiElement As Object, foundOnes As IList(Of Object))
    If TypeOf uiElement Is TextBlock Then
        Dim uiElementAsTextBlock = DirectCast(uiElement, TextBlock)
        If uiElementAsTextBlock.Visibility = Visibility.Visible Then
            foundOnes.Add(uiElementAsTextBlock)
        End If
    ElseIf TypeOf uiElement Is Panel Then
        Dim uiElementAsCollection = DirectCast(uiElement, Panel)
        If uiElementAsCollection.Visibility = Visibility.Visible Then
            For Each element In uiElementAsCollection.Children
                FindTextBlocks(element, foundOnes)
            Next
        End If
    ElseIf TypeOf uiElement Is UserControl Then
        Dim uiElementAsUserControl = DirectCast(uiElement, UserControl)
        If uiElementAsUserControl.Visibility = Visibility.Visible Then
            FindTextBlocks(uiElementAsUserControl.Content, foundOnes)
        End If
    ElseIf TypeOf uiElement Is ContentControl Then
        Dim uiElementAsContentControl = DirectCast(uiElement, ContentControl)
        If uiElementAsContentControl.Visibility = Visibility.Visible Then
            FindTextBlocks(uiElementAsContentControl.Content, foundOnes)
        End If
    ElseIf TypeOf uiElement Is Border Then
        Dim uiElementAsBorder = DirectCast(uiElement, Border)
        If uiElementAsBorder.Visibility = Visibility.Visible Then
            foundOnes.Add(uiElementAsBorder)
            FindTextBlocks(uiElementAsBorder.Child, foundOnes)
        End If
    ElseIf TypeOf uiElement Is Rectangle Then
        Dim uiElementAsRectangle = DirectCast(uiElement, Rectangle)
        foundOnes.Add(uiElementAsRectangle)
    End If
End Sub

Actual results:

XAML control converted to a PDF

zax
  • 844
  • 8
  • 14
  • Thanks Zax. It looks like this is what I was looking for. I'm not able to test your solution though. I gave up converting XAML to PDF a while ago (this was a real-life project with deadline) and created the entire PDF from scratch using Xfinium. It's doing its job nicely. – ata6502 Aug 18 '17 at 18:19
  • Sorry I got you an answer too late to be of help in your project. XAML has many advantages as a flow document design environment, not the least of which is animation and interactivity - so it is really nice to be able to design in XAML and render a true PDF. Going through the printer dialog which requires user interactivity for file naming and printer selection is a non-starter for most. – zax Aug 19 '17 at 18:49
  • I completely agree. I will mark your answer as a solution. I may use it one day. – ata6502 Aug 24 '17 at 19:12