(X) Hide this
    • Login
    • Join
      • Generate New Image
        By clicking 'Register' you accept the terms of use .

Windows 8.1: Create a PDF Viewer with new PDF API

(3 votes)
Andrea Boschin
>
Andrea Boschin
Joined Nov 17, 2009
Articles:   91
Comments:   9
More Articles
1 comments   /   posted on Jan 06, 2014
Categories:   Windows 8

Tweet

Portable Document Format is actually the de-facto standard for document publishing since it is widely adopted by all platforms and used by most of people. Supporting PDF is something that a platform cannot avoid and, since the very first release, Windows 8 comes with an app that can read this format. In the latest release the APIs gained a new namespace dedicated to the PDF format, so now you can easily read and operate with these files. What's the better example than create a little and simple PDF Viewer?

Windows.Data.Pdf

The new API dedicated to the Portable Document Format are all in the Windows.Data.Pdf namespace. They are really simple and basic and only allows to read documents, so please, if your intent is to output a PDF from your app choose one of the free or commercial libraries available.

The root class in the namespace is PdfDocument that is able to read the content from a file (a IStorageFile instance) or directly from a stream enabling the manipulation of in-memory contents. Here is a simple example:

   1: StorageFile file = this.GetFile();
   2: this.Document = await PdfDocument.LoadFromFileAsync(file);

The two static methods, LoadFromFileAsync and LoadFromStreamAsync both allow you to open password protected files providing the password as secondary argument. You only know that a document is protected by a password trying to open it so you have to catch the raised exception and then ask the user for the password.

Once the document has been loaded you have very few information about it. The PdfDocument class only provide the PageCount and a boolean flag set to true if the document is password protected. I would have preferred something more but that's it.

Of course you are able to get every single page as and instance of PdfPage class using the GetPage method that requires the index of the page to extract. The PdfPage class has some more information available including the size of the page, its index in the document and other properties like the rotation and dimensions. Here is how you can get the page:

   1: PdfPage page = this.Document.GetPage(1);
   2:  
   3: var width = page.Size.Width;
   4: var height = page.Size.Height;

In the same snippet you also see how to get the current size of the page form the Size property.

Having a reference to a page is the starting point to render it to the view. The API lets you to render the page to a bitmap so you can then use an Image to display the content to the user. The rendering is almost simple:

   1: await this.CurrentPage.RenderToStreamAsync(stream);

This line is able to render the page to a stream in the PNG format.

Create a simple PDF reader

imageBefore to go deep inside the rendering options that are someway much more powerful that the simple example I’ve provided in the previous snippet, let start a little project that will implement a basic PDF Viewer able to navigate the document and zoom in and out its pages.

First of all let start creating a blank application project with the structure in the figure on the right side. The application only contains a single page that is the surface where the PDF will be displayed. In a fully featured version you should implement share target contract and associate with the .pdf extension but for the purpose of this example I only added a button that allows to pick a pdf file from the file system. The other components are the image rendering surface (a Image element), two buttons used to navigate the pages and a text block to display the current position in the document.

   1: <Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
   2:     <Grid.Resources>
   3:         <Style TargetType="Button">
   4:             <Setter Property="BorderThickness" Value="0" />
   5:         </Style>
   6:     </Grid.Resources>
   7:     <Button x:Name="btnOpenFile" Content="Click to open file" VerticalAlignment="Stretch" HorizontalAlignment="Stretch" />
   8:     <Grid x:Name="grdView" Visibility="Collapsed">
   9:         <Image x:Name="imgRenderSurface" Stretch="Uniform" />
  10:         <Border Background="{ThemeResource ApplicationPageBackgroundThemeBrush}" 
  11:                 Padding="5" VerticalAlignment="Top" HorizontalAlignment="Center">
  12:             <TextBlock FontSize="24" x:Name="txtPage" />
  13:         </Border>
  14:         <Button x:Name="btnBack" HorizontalAlignment="Left" VerticalAlignment="Center" 
  15:                 Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
  16:             <SymbolIcon Symbol="Back" />
  17:         </Button>
  18:         <Button x:Name="btnForward" HorizontalAlignment="Right" VerticalAlignment="Center" 
  19:                 Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
  20:             <SymbolIcon Symbol="Forward" />
  21:         </Button>
  22:     </Grid>
  23: </Grid>

The flow starts when the user clicks the button to open the document. This will start the FileOpenPicker to let the user choose a single pdf file to open. When the file has been choosed the control passes to the OpenFile method that loads the PDF and initializes the rendering environment. Some properties like “ZoomFactor” are initialized but they will be used only in the next step, when I‘ll show how to implement zooming.

   1: private async void btnOpenFile_Click(object sender, RoutedEventArgs e)
   2: {
   3:     FileOpenPicker picker = new FileOpenPicker();
   4:     picker.FileTypeFilter.Add(".pdf");
   5:     StorageFile file = await picker.PickSingleFileAsync();
   6:  
   7:     if (file != null)
   8:         await OpenFile(file);
   9: }
  10:  
  11: private async Task OpenFile(StorageFile file)
  12: {
  13:     try
  14:     {
  15:         this.Document = await PdfDocument.LoadFromFileAsync(file);
  16:     } 
  17:     catch(Exception ex)
  18:     {
  19:         MessageDialog dialog = new MessageDialog(ex.Message);
  20:         dialog.ShowAsync();
  21:         return;
  22:     }
  23:  
  24:     this.CurrentPageIndex = 0;
  25:     this.ZoomFactor = 1.0;
  26:     this.SetState(false);
  27:     await this.RenderPage();
  28: }

After the file has been successfully opened, the flow call the RenderPage method that is the central core that renders the pdf to the image surface. The current version simply render the full page on the surface:

   1: private async Task RenderPage()
   2: {
   3:     this.CurrentPage = this.Document.GetPage((uint)this.CurrentPageIndex);
   4:  
   5:     using (IRandomAccessStream stream = new MemoryStream().AsRandomAccessStream())
   6:     {
   7:         await this.CurrentPage.RenderToStreamAsync(stream);
   8:  
   9:         BitmapImage source = new BitmapImage();
  10:         source.SetSource(stream);
  11:         imgRenderSurface.Source = source;
  12:     }
  13:  
  14:     this.txtPage.Text = string.Format("{0} of {1}", this.CurrentPageIndex + 1, this.Document.PageCount);
  15: }

First of all, the current page is loaded on the basis of the current page index. This is updated by the forward and backward buttons as I’ll show later. Then it is created a MemoryStream and casted to a IRandomAccessStream. This let you to write the content of the page to the stream using the RenderToStreamAsync method. In this way it is not required to create a temporary file on the file system to write the page before to load it in the Image element. Once the stream has been written, it is assigned to the Image element using a BitmapImage. This class is able to load content from the stream and then can be set as source for the image itself.

When the user hits the forward and backward buttons the code simply evaluate the updated current index and check that it is inside the boundaries of document PageCount.

   1: private async void SwitchPage_Click(object sender, RoutedEventArgs e)
   2: {
   3:     if (sender == this.btnBack && this.CurrentPageIndex > 0)
   4:     {
   5:         this.CurrentPageIndex--;
   6:         await this.RenderPage();
   7:     }
   8:     else if (sender == this.btnForward && this.CurrentPageIndex < (int)this.Document.PageCount - 1)
   9:     {
  10:         this.CurrentPageIndex++;
  11:         await this.RenderPage();
  12:     }
  13: }

Implement a simple zoom

The PdfPage class does not only allow to render pages to a PNG bitmap, but is also able to zoom and crop portions of the page and then render it to the bitmap without losing definition. PDF is a vector image format so you can simply determine the desired size and the rendering has always the better definition if the file does not contains embedded bitmap images. To implement zooming you can simply increase the output desired size setting the DestinationHeight and DestinationWidth properties on the PdfPageRenderOptions class. This class is made to change the default behavior of the rendering process and have a number of properties that influences how the rendering happens. Setting the two properties let you determine the size of the output bitmap and so the resulting image will be larger.

These properties can be used in junction with the SourceRect property that instead determine the portion of the page to render on the give size. In the following snippet I’ll use both the features to implement a “strange” zooming that is not the best for the user experience but let me show I a single piece of code how both cropping and zooming works together. Let start adding a bunch of buttons:

   1: <StackPanel Orientation="Horizontal" VerticalAlignment="Bottom" HorizontalAlignment="Right" Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
   2:     <Button x:Name="btnZoomIn">
   3:         <SymbolIcon Symbol="ZoomIn" />
   4:     </Button>
   5:     <Button x:Name="btnZoomOut">
   6:         <SymbolIcon Symbol="ZoomOut" />
   7:     </Button>
   8: </StackPanel>
   9: <Grid x:Name="grdPan" Visibility="Collapsed" VerticalAlignment="Bottom" HorizontalAlignment="Left">
  10:     <Grid.RowDefinitions>
  11:         <RowDefinition Height="Auto" />
  12:         <RowDefinition Height="Auto" />
  13:         <RowDefinition Height="Auto" />
  14:     </Grid.RowDefinitions>
  15:     <Grid.ColumnDefinitions>
  16:         <ColumnDefinition Width="Auto" />
  17:         <ColumnDefinition Width="Auto" />
  18:     </Grid.ColumnDefinitions>
  19:     <Button x:Name="btnUp" Grid.Row="0" Grid.ColumnSpan="2" HorizontalAlignment="Center">
  20:         <SymbolIcon Symbol="Up" />
  21:     </Button>
  22:     <Button x:Name="btnDown" Grid.Row="2" Grid.ColumnSpan="2"  HorizontalAlignment="Center">
  23:         <SymbolIcon Symbol="Download" />
  24:     </Button>
  25:     <Button x:Name="btnLeft" Grid.Row="1" Grid.Column="0">
  26:         <SymbolIcon Symbol="Back" />
  27:     </Button>
  28:     <Button x:Name="btnRight" Grid.Row="1" Grid.Column="1">
  29:         <SymbolIcon Symbol="Forward" />
  30:     </Button>
  31: </Grid>

These are the zoom-in and zoom-out buttons together with 4 buttons used to pan the zoom on the whole page size. The zoom will always have the full page size but it will work as a keyhole on a portion of the page. The pan buttons let you move the hole on the entire surface:

   1: private async void Pan_Click(object sender, RoutedEventArgs e)
   2: {
   3:    if (this.CurrentPage != null)
   4:    {
   5:        double width = this.CurrentPage.Size.Width * (1 / this.ZoomFactor);
   6:        double height = this.CurrentPage.Size.Height * (1 / this.ZoomFactor);
   7:        double x = this.Position.X;
   8:        double y = this.Position.Y;
   9:  
  10:        if (sender == this.btnUp)
  11:        {
  12:            y -= height;
  13:  
  14:            if (y < 0) y = 0;
  15:        }
  16:        else if (sender == this.btnLeft)
  17:        {
  18:            x -= width;
  19:  
  20:            if (x < 0) x = 0;
  21:        }
  22:        else if (sender == this.btnDown)
  23:        {
  24:            y += height;
  25:  
  26:            if (y + height > this.CurrentPage.Size.Height) 
  27:                y = this.CurrentPage.Size.Height - height;
  28:        }
  29:        else if (sender == this.btnRight)
  30:        {
  31:            x += width;
  32:  
  33:            if (x + width > this.CurrentPage.Size.Width) 
  34:                x = this.CurrentPage.Size.Width - width;
  35:        }
  36:  
  37:        this.Position = new Point(x, y);
  38:  
  39:        await this.RenderPage();
  40:    }
  41: }
  42:  
  43: private async void ChangeZoom_Click(object sender, RoutedEventArgs e)
  44: {
  45:    if (sender == this.btnZoomOut && this.ZoomFactor > 1.0)
  46:    {
  47:        this.ZoomFactor -= .5;
  48:        await this.RenderPage();
  49:    }
  50:    else if (sender == this.btnZoomIn && this.ZoomFactor < 5.0)
  51:    {
  52:        this.ZoomFactor += .5;
  53:        await this.RenderPage();
  54:    }
  55:  
  56:    this.grdPan.Visibility = this.ZoomFactor > 1 ? Visibility.Visible : Visibility.Collapsed;
  57: }

After the button logic has been created it is time to change the RenderPage behavior. This now will crop a single piece of the page on the basis of the current zoom factor

   1: private async Task RenderPage()
   2: {
   3:     this.CurrentPage = this.Document.GetPage((uint)this.CurrentPageIndex);
   4:  
   5:     using (IRandomAccessStream stream = new MemoryStream().AsRandomAccessStream())
   6:     {
   7:         if (this.ZoomFactor != 1)
   8:         {
   9:             PdfPageRenderOptions options = new PdfPageRenderOptions();
  10:             options.DestinationHeight = (uint)this.imgRenderSurface.ActualHeight;
  11:             options.DestinationWidth = (uint)this.imgRenderSurface.ActualWidth;
  12:             double width = this.CurrentPage.Size.Width * (1 / this.ZoomFactor);
  13:             double height = this.CurrentPage.Size.Height * (1 / this.ZoomFactor);
  14:             options.SourceRect = new Rect(this.Position, new Point(this.Position.X + width, this.Position.Y + height));
  15:             await this.CurrentPage.RenderToStreamAsync(stream, options);
  16:         }
  17:         else
  18:             await this.CurrentPage.RenderToStreamAsync(stream);
  19:  
  20:         BitmapImage source = new BitmapImage();
  21:         source.SetSource(stream);
  22:         imgRenderSurface.Source = source;
  23:     }
  24:  
  25:     this.txtPage.Text = string.Format("{0} of {1}", this.CurrentPageIndex + 1, this.Document.PageCount);
  26: }

The updated RenderPage method works differently when the ZoomFactor is different from 1.0 (no zoom). It then creates a PdfPageRenderOptions instance and set the DestinationWidth and DestinationHeight properties to the size of the whole page. Then, using the SourceRect it sets a rectangle of a reduced size, and the result is rendered to the image surface. This make the output image being a zoom of the source rectangle without losing in quality.

Try by yourself

The example I presented is available for the download attached to this article. You can try by yourself the code I’ve talked about in the previous paragraphs. The Windows.Data.Pdf namespace will let you add some interesting features to your apps, but please do not expect so much from it since it is a very minimal toolset that I hope will be improved in future releases.


Subscribe

Comments

  • rajiv

    Re: Windows 8.1: Create a PDF Viewer with new PDF API


    posted by rajiv on Feb 26, 2014 09:21

    HI,

     I want to create Pdf file in WIndows 8.1

    Thanks in advance

Add Comment

Login to comment:
  *      *       

From this series