xamlpdfxamarinxamarin.formspdf-conversion

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


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!