There’s a Windows 8 app that I have a minor involvement with that needs to be able to display PDF documents and also capture annotations (ideally ink) on those documents.
I haven’t looked in great detail at how to display PDF documents (other than to launch a file with a .PDF extension and let the Windows file associations do their thing) in a Windows 8 app so I thought I’d do a little light investigation and see what it’s like to work with PDF.
I’m aware of a few SDKs for doing PDF display including;
- The Foxit SDK
- The Leadtools SDK
- ComponentOne control
- The PDFTron Mobile PDF SDK
- muPDF SDK
So I figured what I’d do is to have a look at them in a small amount of time to see how far I could get with each SDK without burning tonnes of cycles on it. Here goes;
The Foxit SDK
Download: here
Size: 20MB
Pricing: Unsure but definitely paid (with additional costs for some of the extra modules I think)
Trial Mode: Yes, for 30-days free.
In the package: You get a zip file with a .vsix installer and then some docs in .chm and .html format along with samples. Without samples, I’d have been lost.
Not being one for reading manuals, I made a blank project inside of Visual Studio and had a look at the toolbox to see if there was a PDF control that I could use and there wasn’t but the Foxit SDK did show up as an extension library that I could reference so I referenced that from my project and doing that also brought in the Visual C++ Runtime Package.
Because that’s now taking a native code dependency, I switched my project to build for x64 rather than “AnyCPU” and that made the project build again but I hit a snag with the XAML designer in that it kept throwing up an exception whenever I had the Foxit SDK referenced;
I’ve no idea why that might be happening but I figured that I may be able to live without the designer so I carried on. I embedded a PDF document into my project’s Assets folder and called it myPdf.PDF and then found that I could open that up via Foxit with;
StorageFile file = await StorageFile.GetFileFromApplicationUriAsync( new Uri("ms-appx:///Assets/MyPDF.pdf")); DocHandle docHandle = await Document.LoadAsync(file, string.Empty);
But then I got a little stuck as to what I was meant to do in order to get the document displayed and two things;
- The API itself is a sort of “flat” C-style API where you are passing lots of handles into static functions. This makes it a little hard from the point of view of discoverability.
- The docs aren’t really written from a scenario point of view but more from a “function X does Y” point of view.
had me yearning for samples so I took a look at a sample and pretty quickly unpicked it in order to give myself a “minimum” demo in that I could author this XAML file;
<Grid> <ScrollViewer> <Image x:Name="myImage" /> </ScrollViewer> </Grid>
and then drop some code behind it;
InstanceHandle hInstance = new InstanceHandle(); Base.InitLibrary(hInstance); // NB: I think I'm meant to call Base.GetLastError() after each of these // function calls a bit like checking HRESULTs in Windows calls. I'm not // doing it though 🙂 Base.UnlockLibrary("SDKEDTEMP", "1E5356D218D3CDDC8C9211AC4323724A554416F3"); Base.LoadSystemFont(); Base.LoadJbig2Decoder(); Base.LoadJpeg2000Decoder(); Base.LoadCMapsGB(); Base.LoadCMapsGBExt(); Base.LoadCMapsCNS(); Base.LoadCMapsJapan(); Base.LoadCMapsJapanExt(); Base.LoadCMapsKorea(); StorageFile file = await StorageFile.GetFileFromApplicationUriAsync( new Uri("ms-appx:///Assets/MyPDF.pdf")); DocHandle docHandle = await Document.LoadAsync(file, string.Empty); // Assume there's at least one page. PageHandle pageHandle = Document.LoadPage(docHandle, 0); await Document.ParsingPageAsync(pageHandle, false); var actualWidth = View.GetPageWidth(pageHandle); var actualHeight = View.GetPageHeight(pageHandle); var scaledHeight = (Window.Current.Bounds.Width / actualWidth) * actualHeight; PixelSource pixelSource = new PixelSource(); pixelSource.Width = (int)Window.Current.Bounds.Width; pixelSource.Height = (int)scaledHeight; var stream = await View.RenderPageAsync(pixelSource, pageHandle, 0, 0, pixelSource.Width, pixelSource.Height, 0, 0, null); WriteableBitmap bitmap = new WriteableBitmap(pixelSource.Width, pixelSource.Height); await bitmap.SetSourceAsync(stream); this.myImage.Source = bitmap;
and that seemed to give me a PDF document where the width matched the width of my screen and the height scrolled to accommodate the content.
That worked ok but if, for instance, I wanted to take all the pages of the document and data-bind them into (e.g.) a FlipView control then I’d have to write quite a bit of code to go from this style of API to an object-oriented API that could work with XAML and did the right thing around loading pages dynamically as they were needed.
I’m sure all of that could be done but it’d take some work.
When it comes to annotations, the SDK does have annotations support for notes, highlights, pencil, stamps, files and links. As far as I can tell though the responsibility for saving these things lies with the developer so it’s hard to work out whether I’d use the SDKs own system or whether I would (e.g.) allow the pen to hand-write over the PDF page and then just save the ink strokes separately so that they can be relayed over the PDF at a later point.
For instance, if I change my UI to include a Canvas;
<Grid> <Image x:Name="myImage" /> <Canvas x:Name="myCanvas" PointerPressed="c_PointerPressed" PointerReleased="c_PointerReleased" PointerMoved="c_PointerMoved" Width="1366" Height="768" Background="Transparent" /> </Grid>
which is on top of the Image that’s being built by the PDF renderer then I could add some event handlers;
public sealed partial class MainPage : Page { public MainPage() { this.InitializeComponent(); this.inkManager = new InkManager(); } async protected override void OnNavigatedTo(NavigationEventArgs e) { InstanceHandle hInstance = new InstanceHandle(); Base.InitLibrary(hInstance); // NB: I think I'm meant to call Base.GetLastError() after each of these // function calls a bit like checking HRESULTs in Windows calls. I'm not // doing it though 🙂 Base.UnlockLibrary("SDKEDTEMP", "1E5356D218D3CDDC8C9211AC4323724A554416F3"); Base.LoadSystemFont(); Base.LoadJbig2Decoder(); Base.LoadJpeg2000Decoder(); Base.LoadCMapsGB(); Base.LoadCMapsGBExt(); Base.LoadCMapsCNS(); Base.LoadCMapsJapan(); Base.LoadCMapsJapanExt(); Base.LoadCMapsKorea(); StorageFile file = await StorageFile.GetFileFromApplicationUriAsync( new Uri("ms-appx:///Assets/MyPDF.pdf")); DocHandle docHandle = await Document.LoadAsync(file, string.Empty); // Assume there's at least one page. PageHandle pageHandle = Document.LoadPage(docHandle, 0); await Document.ParsingPageAsync(pageHandle, false); var actualWidth = View.GetPageWidth(pageHandle); var actualHeight = View.GetPageHeight(pageHandle); var scaledWidth = (Window.Current.Bounds.Height / actualHeight) * actualWidth; PixelSource pixelSource = new PixelSource(); pixelSource.Width = (int)scaledWidth; pixelSource.Height = (int)Window.Current.Bounds.Height; var stream = await View.RenderPageAsync(pixelSource, pageHandle, 0, 0, pixelSource.Width, pixelSource.Height, 0, 0, null); WriteableBitmap bitmap = new WriteableBitmap(pixelSource.Width, pixelSource.Height); await bitmap.SetSourceAsync(stream); this.myImage.Source = bitmap; } void OnPointerPressed(object sender, PointerRoutedEventArgs e) { if (e.Pointer.PointerDeviceType == Windows.Devices.Input.PointerDeviceType.Pen) { this.lastPoint = e.GetCurrentPoint(this.myCanvas); this.inkManager.ProcessPointerDown(e.GetCurrentPoint(this)); this.isDown = true; } } void OnPointerReleased(object sender, PointerRoutedEventArgs e) { if (this.isDown && (e.Pointer.PointerDeviceType == Windows.Devices.Input.PointerDeviceType.Pen)) { this.inkManager.ProcessPointerUp(e.GetCurrentPoint(this)); this.isDown = false; this.DrawInkManagerStrokesToCanvas(); } } void DrawInkManagerStrokesToCanvas() { // Get rid of everything(!) we've drawn so far which is wasteful // and we could do a lot better; this.myCanvas.Children.Clear(); // Ask the ink manager for the strokes, these could be persisted. var strokes = this.inkManager.GetStrokes(); // Draw them back onto the canvas for now foreach (var stroke in strokes) { PathGeometry geometry = new PathGeometry(); PathFigure figure = new PathFigure(); Path path = new Path(); var segments = stroke.GetRenderingSegments(); foreach (var segment in segments) { BezierSegment bezier = new BezierSegment(); bezier.Point1 = segment.BezierControlPoint1; bezier.Point2 = segment.BezierControlPoint2; bezier.Point3 = segment.Position; figure.Segments.Add(bezier); } figure.StartPoint = segments[0].BezierControlPoint1; geometry.Figures.Add(figure); path.Data = geometry; path.Stroke = this.LineBrush; path.StrokeThickness = segments.Average(s => s.Pressure) * 5.0; this.myCanvas.Children.Add(path); } } void OnPointerMoved(object sender, PointerRoutedEventArgs e) { if (this.isDown && (e.Pointer.PointerDeviceType == Windows.Devices.Input.PointerDeviceType.Pen)) { PointerPoint currentPoint = e.GetCurrentPoint(this.myCanvas); this.myCanvas.Children.Add( new Line() { X1 = this.lastPoint.Position.X, Y1 = this.lastPoint.Position.Y, X2 = currentPoint.Position.X, Y2 = currentPoint.Position.Y, Stroke = this.LineBrush, StrokeThickness =currentPoint.Properties.Pressure * 5.0 } ); this.lastPoint = currentPoint; this.inkManager.ProcessPointerUpdate(e.GetCurrentPoint(this)); } } SolidColorBrush LineBrush { get { if (this.lineBrush == null) { this.lineBrush = new SolidColorBrush(Colors.Red); } return (this.lineBrush); } } SolidColorBrush lineBrush; PointerPoint lastPoint; InkManager inkManager; bool isDown; }
Now, there’s a bunch of inefficiency in there and there’s also a bunch of work I’d have to do in order to make this handle different screen sizes and zooming and so on (and make sure I did that in a way that kept the annotations in the same place as they started off) but that’s do-able and the InkManager is doing the job of giving me a set of strokes that I can persist and load back up and re-associate with this one page of PDF in the future.
I could do other kinds of annotations (e.g.) highlights by just having a broad, transparent yellow brush and I could have text annotations as well.
All in all, I’m pretty sure that I could get what I want done with this SDK but I think I’d end up writing my own control which sat on top of it and handled paging and loading/saving annotations.
The Leadtools SDK
Download: here
Size: 773MB
Pricing: From scanning the web page, it looks like it is $3K in order to buy a developer license and then another $250 per seat for the end user.
Trial Mode: There’s a 60-day trial mode
In the package: TBD
I was on a train at the time of writing this post and I couldn’t really download 773MB download so I had to park this one for now. Additionally, if the licensing is as I described it above then it wouldn’t really work for me as I’m not sure how the $250 per end user would work for an application sold via the Windows Store.
The ComponentOne Control
Download: here
Size: 20MB
Pricing: Basic cost looks to be $895 or you can pay $1195 to add support but I think you’re buying more than just the PDF viewing control ( not 100% sure on this one ).
Trial Mode: There’s a free trial, not sure how long it lasts for.
In the package: This one comes as an MSI
I was happy that this one was described as a “control” rather than as some componentry that knew how to read and render PDFs although ultimately that might mean sacrificing some power for some ease of use.
Because it was called a “control”, I made a blank project in Visual Studio and went and took a look at the Toolbox and, sure enough, a whole tonne of controls showed up – date/time pickers, gauges, calendars, flip tiles and all kinds of stuff from the ComponentOne suite.
I took a CPdfViewer control and dropped it onto the design surface. Visual Studio seemed to get dragged down a dark hole for almost a minute and then the control popped up in the designer. The control looks to have a reasonable number of properties on it – I snipped as many as I could from the miscellaneous section of the toolbox in Visual Studio;
With such a range of properties, I was half expecting to find some kind of Source or DocumentSource property but it didn’t seem to be there so I named my control (myViewer) and tried to see if I could write code against it to load up a document.
I started to write code but I’ll admit that it failed and I ended up with a blank screen.
With a XAML file of;
<PdfViewer:C1PdfViewer x:Name="myViewer" HorizontalAlignment="Stretch" VerticalAlignment="Stretch" Width="1366" Height="768"/>
and some code;
var docFile = await StorageFile.GetFileFromApplicationUriAsync( new Uri("ms-appx:///Assets/MyPDF.pdf")); var stream = await docFile.OpenStreamForReadAsync(); this.myViewer.ViewMode = C1.Xaml.PdfViewer.ViewMode.OnePage; await this.myViewer.LoadDocumentAsync(stream); var currentPage = this.myViewer.PageNumber; // This throws an index array out of bounds exception // which is odd as we're already on this page! this.myViewer.GoToPage(currentPage);
I find that I can’t get a PDF document to show up on the screen and I also get that ‘index out of bounds’ exception when trying to move the viewer to the page that it says that it is already on.
I managed to find a sample which led me to take away my Async call above and try a synchronous call;
var docFile = await StorageFile.GetFileFromApplicationUriAsync( new Uri("ms-appx:///Assets/PdfViewer.pdf")); var stream = await docFile.OpenStreamForReadAsync(); this.myViewer.ViewMode = C1.Xaml.PdfViewer.ViewMode.OnePage; this.myViewer.LoadDocument(stream);
but this was then giving me a null reference exception somewhere in a call stack that I couldn’t really identify but the sample from the blog post was working.
I spent quite some time trying to do a diff between the sample and my own code and what I narrowed it down to in the end was that this code crashes;
public MainPage() { this.InitializeComponent(); this.Loaded += (s, e) => { this.LoadDoc(); }; } async void LoadDoc() { var docFile = await StorageFile.GetFileFromApplicationUriAsync( new Uri("ms-appx:///PdfViewer.pdf")); var stream = await docFile.OpenStreamForReadAsync(); this.myViewer.LoadDocument(stream); }
and this code seems to crash less frequently (it did once) and displays the PDF document;
public MainPage() { this.InitializeComponent(); this.LoadDoc(); } async void LoadDoc() { var docFile = await StorageFile.GetFileFromApplicationUriAsync( new Uri("ms-appx:///PdfViewer.pdf")); var stream = await docFile.OpenStreamForReadAsync(); this.myViewer.LoadDocument(stream); }
There’s presumably some kind of timing/ordering going on there which causes one variation to crash and the other to load the document up ok.
It’s a bit less code to get the document on the screen than I wrote against the Foxit SDK and once the document is on the screen the question becomes how to add annotations. The control itself looks to provide automatic scrolling;
which could make it more “challenging” to store ink annotations as I’d need to know exactly where the user was in the document at the time. That said, the control can also provide the PDF document a page at a time by providing you with something derived from FrameworkElement which renders one page. I made a basic attempt at this by taking the C1PdfViewer control out of my Grid in my XAML and then just naming the empty grid so that I could add a child to it at runtime;
<Grid x:Name="myGrid" Background="White"> <!--<PdfViewer:C1PdfViewer x:Name="myViewer" Width="1366" Height="768" />--> </Grid>
plus code;
public MainPage() { this.InitializeComponent(); this.LoadDoc(); } async void LoadDoc() { var docFile = await StorageFile.GetFileFromApplicationUriAsync( new Uri("ms-appx:///PdfViewer.pdf")); var stream = await docFile.OpenStreamForReadAsync(); C1PdfViewer viewer = new C1PdfViewer(); viewer.LoadDocument(stream); var firstPage = viewer.GetPages().First(); this.myGrid.Children.Add(firstPage); }
and that seemed to work well enough but it did make me wonder what would happen if I then zoomed this content so, to test, I dropped a ScrollViewer into my Grid;
<Grid x:Name="myGrid" Background="White"> <ScrollViewer x:Name="myScrollViewer" ScrollViewer.ZoomMode="Enabled"> </ScrollViewer> </Grid>
and tweaked the code a little;
public MainPage() { this.InitializeComponent(); this.LoadDoc(); } async void LoadDoc() { var docFile = await StorageFile.GetFileFromApplicationUriAsync( new Uri("ms-appx:///PdfViewer.pdf")); var stream = await docFile.OpenStreamForReadAsync(); C1PdfViewer viewer = new C1PdfViewer(); viewer.LoadDocument(stream); var firstPage = viewer.GetPages().First(); this.myScrollViewer.Content = firstPage; }
and then played around with zooming and seemed to get pretty decent fidelity out of things – i.e. this isn’t bitmap scaling which is good to know;
Putting the unexplained crash to one side, I think I could build what I want on top of this SDK and get my ink annotations going on top of the pages that the control displays for me.
The PDFTron Mobile PDF SDK
Download: here
Size: TBD
Pricing: TBD
Trial Mode: TBD
In the package: TBD
On this one, I couldn’t actually find the download for the WinRT version of the SDK. There’s definitely a web page but I couldn’t see how I download the WinRT SDK rather than the .NET SDK or the Silverlight SDK and it wasn’t clear to me if they were the same thing so I filled in the form where you can request a trial and I’m awaiting a response on that one.
The muPDF SDK
I came across the muPDF library via David’s blog post here which has good links to the downloads and also to the Libreliodev port of the library.
It looks like muPDF is licensed under GPL but it also seems that there are commercial licenses for it as well ( see http://www.artifex.com/page/licensing-information.html and remember I’m not here to give anyone legal advice around licenses ).
Rather than me dig into it here, take a look at David’s post where he’s written up muPDF.
Wrapping Up
It looks like there are a few options (at least) for building PDF support into a Windows 8 Store application – if I was taking this forward, I’d look at building a control out of one of these frameworks and having that control support pagination, ink annotations by building out some of the code that I have above into something that’s more “production” like. It’d take some work but the hard work is already handled by one of these SDKs.