Creating a PDF Viewer in WPF using Windows 10 APIs

Many applications require interaction with PDF. And there are some amazing libraries out there for displaying and manipulating a PDF file. However most of these libraries are expensive and not suitable for the casual hobbyist.

If you want a good free PDF library, I recommend PDFSharp. However, this only includes PDF manipulations. If you want to display the PDF, you need something else.

Luckily, in UWP, there is the simple but effective PDFDocument API(Windows.Data.Pdf). That makes rendering a PDF a walk in the park. So let's use that API in our WPF application.

You can find all the code here.

Adding Windows 10 APIs

Yes, you can totally use Windows 10 APIs from WPF (or even WinForms). For more information see this blog.

Here is the important part of that blog, these instructions will give us access to the Windows 10 APIs:

  1. Right click on References. Select "Add Reference…" from the context menu. On the left of the Reference Manager, choose Browse and find the following file: C:\Program Files (x86)\Windows Kits\10\UnionMetadata\winmd. Add it to your project as a reference. Note: You will need to change the filter to "All Files".
  2. Right click on References. Select "Add Reference…" from the context menu. On the left of the Reference Manager, go to Browse and find the directory "C:\Program Files (x86)\Reference Assemblies\Microsoft\Framework.NETCore\v4.5". Add System.Runtime.WindowsRuntime.dll to your project.

Windows 10 APIs all start with Windows., where the classic .NET libraries start with .System.

Let's get krakin!

The Plan

To display the PDF in our WPF application we'll go through the following steps:

  1. Load the PfdDocument from a path
  2. Convert each page into a Bitmap
  3. Render the collection of images

Loading the PdfDocument

We can load the PdfDocument from an absolulte path or from a stream. In this example I'll be using a path.

var file = await StorageFile.GetFileFromPathAsync(path);
var pdf = await PdfDocument.LoadFromFileAsync(file);

Notice that the LoadFromFileAsync() method expects a StorageFile. That means we have to Windows.Storage instead of System.IO to gain access to the file.

Loading a page from the PDF document goes like this:

using (var page = pdfDoc.GetPage(index))
{
  //do something
}

Notice that we have to dispose it, once we are done with it.

Converting a Page to a Bitmap

We can convert one PDF page to a BitmapImage like this:

private static async Task<BitmapImage> PageToBitmapAsync(PdfPage page)
{
  BitmapImage image = new BitmapImage();

  using (var stream = new InMemoryRandomAccessStream())
  {
    await page.RenderToStreamAsync(stream);

    image.BeginInit();
    image.CacheOption = BitmapCacheOption.OnLoad;
    image.StreamSource = stream.AsStream();
    image.EndInit();
  }

  return image;
}

The RenderToStreamAsync() method does all the heavy lifting. Notice that we have to use an InMemoryRandomAccessStream to be compatible with the Windows API. The AsStream() method serves as a bridge between classic .NET and .NET Core: It turns the InMemoryRandomAccessStream into a classical Stream. The BitmapImage that we are using is the one defined in System.Windows.Media.Imaging, in other words the one from WPF, not the one from UWP. This is because we need to feed the Bitmap to a WPF Image as an ImageSource.

Rendering the Image

Rendering the image is quite easy. All we need to do is set the source to the BitmapImage we created earlier.

var image = new Image { Source = bitmap };

Bringing it all Together

I decided to turn all of this into a single control to encourage re-use. The resulting XAML looks something like this.

<controls:PdfViewer PdfPath="Assets/vr50hd_manual.pdf"/>

(The vr50hd is an AV mixer by Roland, they are not sponsering this blog. But Roland, if you have a spare one, I'm interested)

Let's make a new UserControl called PDFViewer. It consists out of an ItemsControl that will contain our images. I wrapped this into a ScrollViewer which allows panning.

<UserControl x:Class="WPF_PDFDocument.Controls.PdfViewer"
             ...>
  <ScrollViewer PanningMode="Both"
                Background="DarkGray">
    <ItemsControl x:Name="PagesContainer"/>
  </ScrollViewer>
</UserControl>

First, I'll create a Dependency Property to support the Binding to PdfPath.

public string PdfPath
{
  get { return (string)GetValue(PdfPathProperty); }
  set { SetValue(PdfPathProperty, value); }
}

public static readonly DependencyProperty PdfPathProperty =
    DependencyProperty.Register("PdfPath", typeof(string), typeof(PdfViewer), 
       new PropertyMetadata(null, propertyChangedCallback: OnPdfPathChanged));

Whenever the value for PdfPath changes, I'll have to load the pdf, turn the pages into images and display those images.

private static void OnPdfPathChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
  var pdfDrawer = (PdfViewer)d;

  if (!string.IsNullOrEmpty(pdfDrawer.PdfPath))
  {
    //making sure it's an absolute path
    var path = System.IO.Path.GetFullPath(pdfDrawer.PdfPath);

    StorageFile.GetFileFromPathAsync(path).AsTask()
      //load pdf document on background thread
      .ContinueWith(t => PdfDocument.LoadFromFileAsync(t.Result).AsTask()).Unwrap()
      //display on UI Thread
      .ContinueWith(t2 => PdfToImages(pdfDrawer, t2.Result), TaskScheduler.FromCurrentSynchronizationContext());
  }

}

Notice that we load the PdfDocument in the background to avoid blocking of the UI Thread. The AsTask() method is another bridge between the two environments. The PdfToImages() method is one that we still have to write. But since it involves changing the UI we have to run it on the Main UI thread. This is done by using the current SynchronizationContext.

Once the PDF document is loaded, we can simply loop through the pages, turn them into images an add them to the ItemsControl.

private async static Task PdfToImages(PdfViewer pdfViewer, PdfDocument pdfDoc)
{
  var items = pdfViewer.PagesContainer.Items;
  items.Clear();

  if (pdfDoc == null) return;

  for (uint i = 0; i < pdfDoc.PageCount; i++)
  {
    using (var page = pdfDoc.GetPage(i))
    {
      var bitmap = await PageToBitmapAsync(page);
      var image = new Image
      {
        Source = bitmap,
        HorizontalAlignment = HorizontalAlignment.Center,
        Margin = new Thickness(0, 4, 0, 4),
        MaxWidth = 800
      };
      items.Add(image);
    }
  }
}

The PageToBitmapAsync() method is the one we defined in the beginning of the blog.

And that's it. The result looks like this:

/wpf_pdfviewer.png

Once again, you can find all the code here.