Reading PDF files in Windows 8.1 Store Apps

Posted by: Sumit Maitra , on 8/23/2013, in Category WinRT
Views: 11177
Abstract: Windows 8.1 Store App SDK launched in Build 2013 a few months back has a big bunch of new features, APIs, controls and components. Today we look at the PDF reader component and use it to view the contents of a PDF.

PDF Reading has often involved third party libraries or incomplete components or implementing the entire step ground up. Microsoft never had a PDF SDK solution earlier. But with release of Windows 8.1, WinRT has got itself a spanking new PDF SDK. This opens up possibilities for reusing a lot of rich content in your Windows 8 Store Apps. Specifically rendering Electronic Magazines that are already released as PDFs. Today we will see how we can convert each page of a magazine to a PNG and then render it on screen. As a sample, we will be taking our own DNC .NET Magazine authored by DotNetCurry authors

The DotNetCurry Magazine (DNC Mag) Pdf Reader

We start off with the Split View project template that gives us a Home Page full of tiles and a Details page that has a List and Details section.

 

new-split-app

The data comes from a SampleData.json file in the DataModel folder.

The Overall App Implementation

We want the first tile on the Home Page to show an “Open File” tile, clicking on which we should bring up a File Picker to select PDF files. Once the PDF is opened, we convert each page into an image and save them in the temporary cache of our application. We set the path of the image in the data source and bind it to the ListView for it to render the Images.

Updating the Sample Data Source

The Sample Data Source has a big set of data. We don’t actually need the sample and we should be creating our own data source. But for this demo, we’ll simply change the properties to more appropriate names and run with it.

We trim the SampleData.json to one group and a bunch of Items. The SampleGroup object property names are modified to be FileName, FilePath, ImagePath, LasAccessed (reserved for future use in case of a real Most Recently Used (MRU) list). The SampleGroupItem object is trimmed down to just UniqueId, PageNumber and ImagePath. The JSON data is as follows

"Groups":[
  {
    "UniqueId": "Group-1",
    "FileName": "Open File",
    "FilePath": "Open an existing PDF file",
    "ImagePath": "Assets/DarkGray.png",
    "Items":
    [
      {
        "UniqueId": "Group-1-Item-1",
        "PageNumber": "Item FileName: 1",
        "ImagePath": "Assets/LightGray.png"
      },
      {
        "UniqueId": "Group-1-Item-2",
        "PageNumber": "Item FileName: 1",
        "ImagePath": "Assets/DarkGray.png"
      },
      {
        "UniqueId": "Group-1-Item-3",
        "PageNumber": "Item FileName: 1",
        "ImagePath": "Assets/MediumGray.png"
      },
      {
        "UniqueId": "Group-1-Item-4",
        "PageNumber": "Item FileName: 1",
        "ImagePath": "Assets/DarkGray.png"
      },
      {
        "UniqueId": "Group-1-Item-5",
        "PageNumber": "Item FileName: 1",
        "ImagePath": "Assets/MediumGray.png"
      }
    ]
  }]

The corresponding source objects are modified as follows:

public class SampleDataItem
{
    public SampleDataItem(String uniqueId, String pageNumber, String imagePath)
    {
        this.UniqueId = uniqueId;
        this.PageNumber = pageNumber;
        this.ImagePath = imagePath;
    }

    public string UniqueId { get; private set; }
    public string PageNumber { get; private set; }
    public string ImagePath { get; private set; }
    public override string ToString()
    {
        return this.PageNumber;
    }
}

The Container Group used in the HomePage is as follows:

public class SampleDataGroup
{
    public SampleDataGroup(String uniqueId, String title, String subtitle, String imagePath, DateTime lastAccessed
    {
        this.UniqueId = uniqueId;
        this.FileName = title;
        this.FilePath = subtitle;
        this.LastAccessed = lastAccessed;
        this.ImagePath = imagePath;
        this.Items = new ObservableCollection<SampleDataItem>();
    }

    public string UniqueId { get; private set; }
    public string FileName { get; private set; }
    public string FilePath { get; private set; }
    public DateTime LastAccessed { get; private set; }
    public string ImagePath { get; private set; }
    public ObservableCollection<SampleDataItem> Items { get; private set; }

    public override string ToString()
    {
        return this.FileName;
    }
}

We have to update the GetSampleDataAsync method that deserializes the JSON data also. We update the appropriate keys and constructors. The final code is as follows:

private async Task GetSampleDataAsync()
{
if (this._groups.Count != 0)
  return;
Uri dataUri = new Uri("ms-appx:///DataModel/SampleData.json");
StorageFile file = await StorageFile.GetFileFromApplicationUriAsync(dataUri);
string jsonText = await FileIO.ReadTextAsync(file);
JsonObject jsonObject = JsonObject.Parse(jsonText);
JsonArray jsonArray = jsonObject["Groups"].GetArray();
foreach (JsonValue groupValue in jsonArray)
{
  JsonObject groupObject = groupValue.GetObject();
  SampleDataGroup group = new SampleDataGroup(groupObject["UniqueId"].GetString(),
  groupObject["FileName"].GetString(),
  groupObject["FilePath"].GetString(),
  groupObject["ImagePath"].GetString(),
  DateTime.Now);
  foreach (JsonValue itemValue in groupObject["Items"].GetArray())
  {
   JsonObject itemObject = itemValue.GetObject();
   group.Items.Add(new SampleDataItem(itemObject["UniqueId"].GetString(),
   itemObject["PageNumber"].GetString(),
   itemObject["ImagePath"].GetString()));
  }
  this.Groups.Add(group);
}
}

Updating the XAML Markup

We update the Bindings in the ItemsPage.xaml to use FileName and FilePath instead of Title and Description.

The ItemsPage.xaml looks as follows at Design Time

items-page-xaml

The changes to SplitPage.xaml are more extensive. We simply keep a 120px left padding for the back button and allocate the rest of the page to the ListView. From the ListView’s data template, we remove all other controls and keep the Image only. The final view at Design Time is as follows:

split-page-xaml

Opening and Reading PDF

After setting our application up, time to implement the File Open and read logic.

Updating the ItemView_ItemClick event

In the ItemsPage.xaml, when user clicks on any of the Tiles, the event is raised. We update the code to verify if the group.FileName property is set to “Open File”. If it is, we create a FileOpenPicker instance and set the Filter to PDFs.

async void ItemView_ItemClick(object sender, ItemClickEventArgs e)
{
    SampleDataGroup group = (SampleDataGroup)e.ClickedItem;
    StorageFile file = null;
    if (group.FileName == "Open File")
    {
        FileOpenPicker filePicker = new FileOpenPicker();
        filePicker.FileTypeFilter.Add(".pdf");
        filePicker.ViewMode = PickerViewMode.Thumbnail;
        filePicker.SuggestedStartLocation = PickerLocationId.DocumentsLibrary;
        filePicker.SettingsIdentifier = "picker1";
        filePicker.CommitButtonText = "Open Pdf File";

        file = await filePicker.PickSingleFileAsync();
    }
    else
    {
        //var groupId = ((SampleDataGroup)e.ClickedItem).UniqueId;
        // TODO: Implement MRU functionality
    }
    if (file != null)
    {
        this.Frame.Navigate(typeof(SplitPage), file);
    }
}

If the user selected a File, we navigate to the SplitPage and pass the StorageFile handle to it.

In Future, when we have real Most Recently Used lists (MRUs), we’ll have an image of the first page and we can open the file directly. But the first item will always remain as the “Open File Tile”.

Loading the File

After navigating to the SplitPage, we restore the StorageFile handle and call the LoadPdfFileAsync that does the load and conversion to PNG images.

private async void navigationHelper_LoadState(object sender, LoadStateEventArgs e)
{
    StorageFile selectedFile = e.NavigationParameter as StorageFile;
    if (selectedFile != null)
    {
        await LoadPdfFileAsync(selectedFile);
    }
}

The PdfDocument object

PdfDocument is the class that encapsulates PDF manipulation for us. We create an instance of it by Load the file using a static helper method:

PdfDocument pdfDocument = await PdfDocument.LoadFromFileAsync(pdfFile);

Next we initialize an observable collection of SampleDataItem objects and set it to the DefaultViewModel

ObservableCollection<SampleDataItem> items = new ObservableCollection<SampleDataItem>();
this.DefaultViewModel["Items"] = items;

Next we check if the PDF has atleast one page and we start looping through the pages. We get to a page using the GetPage call on the PdfDocument object

var pdfPage = pdfDocument.GetPage((uint)pageIndex);

To convert to PNGs, we get the TempFolder for the app and create a new File handle. Next we open a random access stream to that File.

StorageFolder tempFolder = ApplicationData.Current.TemporaryFolder;
StorageFile pngFile = await tempFolder.CreateFileAsync(Guid.NewGuid().ToString() + ".png", CreationCollisionOption.ReplaceExisting);
IRandomAccessStream randomStream = await pngFile.OpenAsync(FileAccessMode.ReadWrite);

Before writing to the PNG stream, we can setup PDF rendering options. I have done a crude calculation of the Width of the Image control in the List and I pass that as the destination width.

PdfPageRenderOptions pdfPageRenderOptions = new PdfPageRenderOptions();

pdfPageRenderOptions.DestinationWidth = (uint)(this.ActualWidth - 130);

Finally we use the pdfPage’s RenderToStreamAsync API to write the PNG file out. Once the file is flushed, we add a new item to the data source. Since this is an observable collection, we’ll start seeing ‘pages’ as they come in.

await pdfPage.RenderToStreamAsync(randomStream, pdfPageRenderOptions);
await randomStream.FlushAsync();
randomStream.Dispose();
pdfPage.Dispose();
items.Add(new SampleDataItem(
    pageIndex.ToString(),
    pageIndex.ToString(),
    pngFile.Path));

And that’s about it! Let’s run the app and see how it works.

PDF Reader Demo Time

The Launch page is rather boring at the moment as it looks like this:

demo-home-page

When we click on the “Open File” tile, the File Picker starts off at the suggested Documents folder and shows the PDF files available.

demo-file-open

We have here the Anniversary issue of DNC Magazine so when we select and click on Open Pdf File, we get the following:

demo-first-page

If you hover mouse over the List or tap it, you will see that the scroll bar is rather large implying not all pages are loaded, but it progressively gets smaller as more pages load. As seen below, we have a screen shot of another page from the Magazine.

demo-another-page

Pretty darn amazing!

Conclusion

That was a quick introduction to the new PdfDocument API that’s a part of the upcoming Windows 8.1 SDK for Store Apps ‘fondly’ known as WinRT. This demo got my imagination running wild with things we can do with PDFs now, hope the article was able to inspire you as well!

Download the entire source code of this article (Github)

Give a +1 to this article if you think it was well written. Thanks!
Recommended Articles
Sumit is a .NET consultant and has been working on Microsoft Technologies since his college days. He edits, he codes and he manages content when at work. C# is his first love, but he is often seen flirting with Java and Objective C. You can follow him on twitter at @sumitkm or email him at sumitkm [at] gmail


Page copy protected against web site content infringement by Copyscape


User Feedback
Comment posted by John Moody on Wednesday, September 4, 2013 8:41 PM
I would rather use PDF Reader for Windows 8 on my Windows 8.1, http://www.pdfeight.com/reader.html
Comment posted by Manivel on Thursday, October 3, 2013 4:10 AM
Hi Sumit,

It is cool app with source code. But we can not zoom in/out in this app.
I need zoom in/out option without blurred text in the PDF page.

Please help me.
Comment posted by Anon on Friday, November 15, 2013 1:34 PM
Thanks Sumit for sharing the code.
Comment posted by Mani on Thursday, November 21, 2013 1:13 AM
Hi Sumit,

This is nice article. I want load two PDF pages in a single item. how do we do that?
Comment posted by shradha on Saturday, July 26, 2014 2:20 AM
why is only a single page being shown?it doesn't get in all the pages of the file!
And thanks a ton!
was very helpful:)

Post your comment
Name:  
E-mail: (Will not be displayed)
Comment:
Insert Cancel