Converting XAML to PDF and Paginating it for a Xamarin.Forms UWP Project

Issue

Until recently I have been stuck on how to achieve the goal of “exporting” a report from a StackLayout into a PDF in a project I somehow pulled out of Dev Limbo.

–BackStory–

Previously I have tried to continue the use of the already placed (in the project) PDFSharp package to convert the data presented in the XAML to a PDF for a client. Long story short, I was unable to get PDFSharp to do what I needed it to do and turned to Syncfusion. They seemed to have the features I needed to make this happen. Going based off the code samples they had, I was able to get close to my goal, but not quite. They have the capture portion and they have the pagination portion, but not a combination of the two. I essentially needed to paginate the screenshot that CaptureAsync() saves to make a pdf of the entire report.

Solution

–How was this resolved?–

After doing some digging, I came across an answer in this article (I am forever grateful) and forged a solution using it.

Here’s a sample of my XAML content page for context:

<?xml version"1.0" encoding"utf-8"?>
<ContentPage xmlns"http://xamarin.com/schemas/2014/forms"
             xmlns:x"http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:controls"clr-namespace:ReportTool.Controls"
             x:Class"ReportTool.ReportViewer">
    <ContentPage.Content>
        <StackLayout Style"{StaticResource TopLevelStackLayout}">

            <!-- Body Block -->
            <Grid x:Name"MainGrid"  Style"{StaticResource MainContainingGrid}">
                <Grid.ColumnDefinitions>
                    <ColumnDefinition Width"1*" />
                </Grid.ColumnDefinitions>
                <Grid.RowDefinitions>
                    <RowDefinition Height"1*" />
                </Grid.RowDefinitions>
                <ScrollView x:Name"MainScrollLayout" VerticalOptions"Fill" HorizontalOptions"Fill" Grid.Row"0" Grid.Column"0" BackgroundColor"#FFFFFF" MinimumWidthRequest"700">
                    <StackLayout x:Name"MainStackLayout" Style"{StaticResource MainBkg}">

                        <Button x:Name"DownloadPdfBtn"  Text"Export to PDF" Clicked"DownloadPdfBtn_OnClicked" TextColor"White" BackgroundColor"DodgerBlue" VerticalOptions"Start" HorizontalOptions"Start" />

                        <Image Source"~\..\Assets\Logos\CompanyLogo.png" Margin"0,60,0,10" HorizontalOptions"Center" />
                        <Label x:Name"TitlePageTitleText" Style"{StaticResource ReportViewerTitleTextMain}" Text"{StaticResource CompanyAnalysisReport}" />
                        <Label x:Name"TitlePagePreparedFor" Style"{StaticResource ReportViewerTitleTextMiddle}" Text"{StaticResource PreparedFor}" />
                        <Label x:Name"TitlePageOrganizationName" Style"{StaticResource ReportViewerTitleTextMiddle}" />
                        <Label x:Name"TitlePageOrganizationAddress1" Style"{StaticResource ReportViewerTitleTextMiddle}" />
                        <Label x:Name"TitlePageOrganizationAddress2" Style"{StaticResource ReportViewerTitleTextMiddle}" />
                        <Label x:Name"TitlePageOrganizationCityStateZip" Style"{StaticResource ReportViewerTitleTextLast}" />

                        <Grid x:Name"ReportGrid" Style"{StaticResource ReportGridBody}">
                            <Grid.ColumnDefinitions>
                                <ColumnDefinition Width"90"/>
                                <ColumnDefinition Width"1*"/>
                                <ColumnDefinition Width"125"/>
                                <ColumnDefinition Width"Auto"/>
                                <ColumnDefinition Width"Auto"/>
                                <ColumnDefinition Width"1*"/>
                            </Grid.ColumnDefinitions>
                            <Grid.RowDefinitions>
                                <RowDefinition Height"Auto"/>
                            </Grid.RowDefinitions>
                        </Grid>

                    </StackLayout>
                </ScrollView>
            </Grid>
        </StackLayout>
    </ContentPage.Content>
</ContentPage>

Here is the code for the star of the show, the ExportToPdf button:

using Syncfusion.Drawing;
using Syncfusion.Pdf;
using Syncfusion.Pdf.Graphics;

private async void DownloadPdfBtn_OnClicked(object sender, EventArgs e)
{
  try
  {
     var filename  "SurveyReport_" + ((App)Application.Current).CurrentUser.UserName + "_" + DateTime.UtcNow.ToString("MMddyy") + ".pdf";

     // Init Memory Stream.
     var stream  new MemoryStream();

     //Create a new PDF document
     using (var document  new PdfDocument())
     {
       // Add page to the PDF document.
       var page  document.Pages.Add();

      // Get the scroll view height.
      var xamlPageHeight  MainScrollLayout.ContentSize.Height;

      // Get the page dimensions.
      var pageWidth  page.GetClientSize().Width;
      var pageHeight  page.GetClientSize().Height;

      // Capture the number of pages.
      var numberOfPages  (int)Math.Ceiling(xamlPageHeight / pageHeight);

      for (var i  0; i < numberOfPages; i++)
      {
        // Find beginning of page.
        await MainScrollLayout.ScrollToAsync(0, i * pageHeight, false).ConfigureAwait(false);

        // Capture the XAML page as an image and returns the image in memory stream.
        var byteData  await DependencyService.Get<IExportPdf>().CaptureAsync();
        var imageStream  new MemoryStream(byteData);

        // Load the image in PdfBitmap.
        var pdfBitmapImage  new PdfBitmap(imageStream);

        // Set the pdf page settings.
        document.PageSettings.Margins.All  0;
        document.PageSettings.Orientation  PdfPageOrientation.Portrait;
        document.PageSettings.Size  new SizeF(pageWidth, pageHeight);

        // Add new page for graphics (otherwise graphics won't know where to draw the rest of the image)
        page  document.Pages.Add();

        // Graphics for drawing image to pdf.
        var graphics  page.Graphics;

        // Draw the image to the page.
        graphics.DrawImage(pdfBitmapImage,0,0, pageWidth, pageHeight);

        // Insert page at i position.
        document.Pages.Insert(i, page);

        // Save the document into memory stream.
        document.Save(stream);
      }
     }

     stream.Position  0;

     // Save the stream as a file in the device and invoke it for viewing.
     await Xamarin.Forms.DependencyService.Get<IExportPdf>().Save(filename, "application/pdf", stream);
    }
    catch (Exception ex)
    {
        DisplayErrorAlert("DownloadPdfBtn_OnClicked", ex.StackTrace);
    }
}

It is important to note that you will need a dependency in order to save anywhere other than local memory. Thankfully, Syncfusion provides a snippet for you to use. For the sake of your time, I will share the snippets. You will need to add two .cs files, one class file with the capture/save functionality and one interface file for your app.

Capture/Save class:

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices.WindowsRuntime;
using System.Threading.Tasks;
using Windows.Graphics.Display;
using Windows.Graphics.Imaging;
using Windows.Storage;
using Windows.Storage.Pickers;
using Windows.Storage.Streams;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Media.Imaging;
using Xamarin.Forms;

public class ExportPdf : IExportPdf
{
    public async Task<byte[]> CaptureAsync()
    {
        var renderTargetBitmap  new RenderTargetBitmap();
        await renderTargetBitmap.RenderAsync(Window.Current.Content);

        var pixelBuffer  await renderTargetBitmap.GetPixelsAsync();
        var pixels  pixelBuffer.ToArray();

        var displayInformation  DisplayInformation.GetForCurrentView().LogicalDpi;

        var stream  new InMemoryRandomAccessStream();

        var encoder  await BitmapEncoder.CreateAsync(BitmapEncoder.JpegEncoderId, stream);
        encoder.SetPixelData(
            BitmapPixelFormat.Bgra8, 
            BitmapAlphaMode.Ignore, 
            (uint)renderTargetBitmap.PixelWidth, 
            (uint)renderTargetBitmap.PixelHeight, 
            displayInformation,
            displayInformation, 
            pixels);
        await encoder.FlushAsync();

        stream.Seek(0);
        var readStream  stream.AsStreamForRead();
        var bytes  new byte[readStream.Length];
        await readStream.ReadAsync(bytes, 0, bytes.Length);

        return bytes;
    }

    public async Task Save(string filename, string contentType, MemoryStream stream)
    {
        if (Device.Idiom ! TargetIdiom.Desktop)
        {
            var local  ApplicationData.Current.LocalFolder;
            var outFile  await local.CreateFileAsync(filename, CreationCollisionOption.ReplaceExisting);
            using (var outStream  await outFile.OpenStreamForWriteAsync()) { await outStream.WriteAsync(stream.ToArray(), 0, (int)stream.Length); }

            if (contentType ! "application/html") await Windows.System.Launcher.LaunchFileAsync(outFile);
        }
        else
        {
            StorageFile storageFile  null;
            var savePicker  new FileSavePicker
            {
                SuggestedStartLocation  PickerLocationId.Desktop,
                SuggestedFileName  filename
            };
            switch (contentType)
            {
                case "application/vnd.openxmlformats-officedocument.presentationml.presentation":
                    savePicker.FileTypeChoices.Add("PowerPoint Presentation", new List<string> { ".pptx" });

                    break;

                case "application/msexcel":
                        savePicker.FileTypeChoices.Add("Excel Files", new List<string> { ".xlsx" });

                    break;

                case "application/msword":
                        savePicker.FileTypeChoices.Add("Word Document", new List<string> { ".docx" });

                    break;

                case "application/pdf":
                        savePicker.FileTypeChoices.Add("Adobe PDF Document", new List<string> { ".pdf" });

                    break;
                case "application/html":
                        savePicker.FileTypeChoices.Add("HTML Files", new List<string> { ".html" });

                    break;
            }

            storageFile  await savePicker.PickSaveFileAsync();

            using (var outStream  await storageFile.OpenStreamForWriteAsync())
            {
                await outStream.WriteAsync(stream.ToArray(), 0, (int)stream.Length);
                await outStream.FlushAsync();
                outStream.Dispose();
            }

            stream.Flush();
            stream.Dispose();
            await Windows.System.Launcher.LaunchFileAsync(storageFile);
        }
    }
}

Interface:

using System.IO;
using System.Threading.Tasks;

public interface IExportPdf
{
    Task Save(string filename, string contentType, MemoryStream stream);
    Task<byte[]> CaptureAsync();
}

And that should do it! I hope this helps anyone that has been tasked with something similar!

Answered By – Wolfie_Mk6

Leave a Comment