Table of Contents
Introduction
Similar to wmjordan, who wrote the CodeProject article Rendering PDF Documents with Mupdf and P/Invoke in C#, I was looking for a free, native .NET PDF rendering engine. Like him, I did not find any and so I used his solution, which uses MuPdf to render PDF pages as images.
Based on his code, I wrote the WPF user control MoonPdfPanel
, which can be used to display PDF files in a .NET based application with minimal effort. To demonstrate the use of MoonPdfPanel
, I wrote a sample WPF application named MoonPdf
. MoonPdf
can be considered as a very basic PDF viewer/reader. It uses the MoonPdfLib
assembly, which contains the mentioned MoonPdfPanel
. The above screenshot shows the MoonPdf
application with a loaded sample PDF file.
In this article, I will show how the MoonPdfPanel
works and how you can integrate it in your application to display PDF files.
Related and Helpful Projects
There are two CodeProject articles that helped me a lot with the creation of MoonPdf
:
As stated above, the first article helped me with the usage of MuPdf for rendering PDF pages as images. The second article provided a useful solution for data virtualization in WPF. This code was used to implement a virtualizing panel, that made a continuous page layout for PDF pages possible. It allowed me to virtualize the PDF pages, i.e. it is not necessary to load all pages at once. This increased performance and decreased memory consumption. I will explain the details on these implementations later.
Building and Including the MuPdf Rendering Engine
For the rendering of the PDF pages, I used the MuPdf rendering engine. MuPdf is also used in the well known PDF reader SumatraPDF. The guys of SumatraPDF already did a great job in providing nmake
files that build a DLL from the MuPdf sources. So in the end, I included the source code of SumatraPDF to build a MuPdf DLL (libmupdf.dll). This DLL is necessary to use the solution proposed in Rendering PDF Documents with Mupdf and P/Invoke in C#.
To include the DLL in the build process, I wrote a small nmake
file, that builds and copies the libmupdf.dll accordingly. The following source code shows the msbuild
file, that is used to compile the complete source code. Before the MoonPdf solution is built, the MuPdf sources are built using my nmake
file makefile-mupdf.msvc (not shown here). After that, the compiled libmupdf.dll (bold in the sources below) is copied accordingly.
<Project DefaultTargets="Build"
xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<platform Condition="$(platform) == ''">X86</platform>
</PropertyGroup>
<Target Name="Build">
<RemoveDir Directories="ext/sumatra/output/$(platform)" />
<exec Command="nmake -f makefile-mupdf.msvc platform=$(platform)"/>
<Copy SourceFiles="ext/sumatra/output/$(platform)/libmupdf.dll"
DestinationFolder="bin/MuLib/$(platform)" />
<MSBuild Projects="src/MoonPdf.sln" Targets="Rebuild"
Properties="Configuration=Release;Platform=$(platform);AllowUnsafeBlocks=true"/>
</Target>
</Project>
Rendering PDF Pages
The rendering of the PDF pages is pretty straight forward. It all takes place in the MuPdfWrapper
class. The main part is done in the ExtractPage
method, which is shown below. The method expects an IPdfSource
object (see below code), the page number that should be rendered and the zoom factor that should be applied for the rendering. The method returns a Bitmap
object, which is later on converted into a BitmapSource
object, to use it in WPF (see the next section).
public static Bitmap ExtractPage(IPdfSource source, int pageNumber,
float zoomFactor = 1.0f)
{
var pageNumberIndex = Math.Max(0, pageNumber - 1);
using (var stream = new PdfFileStream(source))
{
IntPtr p = NativeMethods.LoadPage(stream.Document, pageNumberIndex);
var bmp = RenderPage(stream.Context, stream.Document, p, zoomFactor);
NativeMethods.FreePage(stream.Document, p);
return bmp;
}
}
MoonPdf allows loading PDF documents from file or from memory. The above mentioned interface IPdfSource
is the common interface for the two sources (FileSource
and MemorySource
).
Most of the unmanaged resources were encapsulated into the class PdfFileStream
, which implements the IDisposable
interface. The use of the PdfFileStream
object is shown in the using
statement above. For the rendering, the ExtractPage
method makes use of the RenderPage
method. This method (and the rest of the interop code) can be looked up here. I only made a slight modification to the RenderPage
method, to take the zoom factor into account. The modifications are shown below. I omitted the rest of the (not so short) method for clarity reasons.
static Bitmap RenderPage(IntPtr context, IntPtr document, IntPtr page, float zoomFactor)
{
...
Rectangle pageBound = NativeMethods.BoundPage(document, page);
int width = (int)(pageBound.Width * zoomFactor);
int height = (int)(pageBound.Height * zoomFactor);
Matrix ctm = new Matrix();
ctm.A = zoomFactor;
ctm.D = zoomFactor;
...
}
Well, that's pretty much everything about the rendering of the PDF pages. Later, I will show where the ExtractPage
method is called, to display the rendered images.
Displaying the Rendered PDF Pages
Basics
Before I explain further details, I need to clarify some terms that I will use further on:
- PDF page: A page from the PDF document that gets rendered as a bitmap.
- Page row: A collection of one or two PDF pages (two PDF pages are displayed side by side).
In MoonPdf, the view types of a page row are addressed with the enum
ViewType
:
public enum ViewType
{
SinglePage,
Facing,
BookView
}
ViewType.SinglePage
is the simplest case, where a PDF page and a page row are identical, which means that one page row only contains one PDF page. ViewType.Facing
means that every page row contains two PDF pages (except there is only one PDF page left to fit in the page row). ViewType.BookView
is the same as ViewType.Facing
, except that it starts with a single PDF page in the first page row.
To illustrate the view types, we look at the following figure. It shows a sample PDF in MoonPdf with the view type ViewType.Facing
. The figure therefore shows one page row with two PDF pages.
Besides the view type, the second important layout aspect is the way the page rows are displayed. This behaviour is adressed with the enum
PageRowDisplayType
(see code below). The value PageRowDisplayType.SinglePageRow
is used to display only one page row at a time. This is shown in the figure above. The other option is PageRowDisplayType.ContinuousPageRows
, which shows page rows continuously. An example for this display type is shown in the first sample figure.
public enum PageRowDisplayType
{
SinglePageRow = 0,
ContinuousPageRows
}
The layout logic for the two page row types is very different, so I decided to implement a user control for each of these types. I created the two user controls SinglePageMoonPdfPanel.xaml and ContinuousMoonPdfPanel.xaml. Although their behaviour is different, they have one thing in common, namely that a page row always contains one or two PDF pages side by side. The easiest way to implement this was to use a ItemsControl
and define its ItemsPanel
as a StackPanel
with horizontal orientation. The items of the ItemsControl
would be Image
objects that would contain the rendered PDF pages as images. I encapsulated the common XAML for both user controls into a global ResourceDictionary
named GlobalResources.xaml, so that it can be used by both user controls. This XAML is shown below. The XAML also contains data bindings for the source and the margin of the image. I will explain their use later.
<ResourceDictionary ...>
<Style x:Key="moonPdfItems" TargetType="{x:Type ItemsControl}">
<Setter Property="ItemTemplate">
<Setter.Value>
<DataTemplate>
<Image Source="{Binding ImageSource}" Margin="{Binding Margin}"
HorizontalAlignment="Center"
UseLayoutRounding="True" Stretch="None"
RenderOptions.BitmapScalingMode="NearestNeighbor" />
</DataTemplate>
</Setter.Value>
</Setter>
<Setter Property="ItemsPanel">
<Setter.Value>
<ItemsPanelTemplate>
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center" />
</ItemsPanelTemplate>
</Setter.Value>
</Setter>
</Style>
</ResourceDictionary>
The layout for the SinglePageMoonPdfPanel
is pretty straight forward, because it only contains one page row at a time. Therefore we only need one ItemsControl
that manages the PDF pages of this one page row. The ItemsControl
and the style that it uses (see XAML above) are in bold in the XAML below. The ControlTemplate
uses a ScrollViewer
element that allows the scrolling of the content. On the ScrollViewer
I set the FocusVisualStyle
to {x:Null}
, to remove the dashed rectangle around the control that is normally shown when the control is focused.
<UserControl x:Class="MoonPdfLib.SinglePageMoonPdfPanel">
<UserControl.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="GlobalResources.xaml" />
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</UserControl.Resources>
<ItemsControl x:Name="itemsControl" ItemsSource="{Binding}"
Style="{StaticResource moonPdfItems}">
<ItemsControl.Template>
<ControlTemplate TargetType="{x:Type ItemsControl}">
<ScrollViewer FocusVisualStyle="{x:Null}">
<ItemsPresenter />
</ScrollViewer>
</ControlTemplate>
</ItemsControl.Template>
</ItemsControl>
</UserControl>
Compared to the above SinglePageMoonPdfPanel
, the ContinuousMoonPdfPanel
contains multiple page rows, so here we need an additional ItemsControl
that manages the multiple page rows. The code below shows the XAML of the ContinuousMoonPdfPanel
. I have highlighted the two ItemsControl
s in bold that are used. The first one is responsible for the page rows. The second one is responsible to display the images side by side. It uses the style with the key moonPdfItems
from the GlobalResources.xaml.
<UserControl x:Class="MoonPdfLib.ContinuousMoonPdfPanel"
xmlns:virt="clr-namespace:MoonPdfLib.Virtualizing" ...>
<UserControl.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="GlobalResources.xaml" />
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</UserControl.Resources>
<ItemsControl Name="itemsControl">
<ItemsControl.Template>
<ControlTemplate TargetType="{x:Type ItemsControl}">
<ScrollViewer CanContentScroll="True" FocusVisualStyle="{x:Null}">
<ItemsPresenter />
</ScrollViewer>
</ControlTemplate>
</ItemsControl.Template>
<ItemsControl.ItemTemplate>
<DataTemplate>
<ItemsControl ItemsSource="{Binding}"
Style="{StaticResource moonPdfItems}">
<ItemsControl.Template>
<ControlTemplate TargetType="{x:Type ItemsControl}">
<ItemsPresenter />
</ControlTemplate>
</ItemsControl.Template>
</ItemsControl>
</DataTemplate>
</ItemsControl.ItemTemplate>
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<virt:CustomVirtualizingPanel />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
</ItemsControl>
</UserControl>
An interesting part of tabove code is the use of the CustomVirtualizingPanel
(in bold). This panel inherits from System.Windows.Controls.VirtualizingPanel
and allows us to virtualize page rows. This means we only need to load the current page rows into memory. This also allows us to dispose previous page rows, when the user scrolls further in the document. This virtualizing panel is so important, because in a "normal" items panel, we would have to load and add all the PDF pages before displaying them. This would increase memory consumption dramatically and our application would be unusable for bigger PDF files. But with virtualization, the memory consumption stays within borders. The virtualization will be discussed later and I'll continue to explain a bit more about the controls.
Because I created two separate controls for single page and continuous layout, I needed a way to wrap them in one user control that can be integrated easily. So I created some kind of a "wrapper" panel named MoonPdfPanel
, which includes the appropriate user control (SinglePageMoonPdfPanel
or ContinuousMoonPdfPanel
) depending on the chosen PageRowDisplayType
. To make this possible, the two mentioned user controls needed some common base or interface, so I decided to create an interface IMoonPdfPanel
that is implemented by these two user controls. The interface is shown below.
internal interface IMoonPdfPanel
{
ScrollViewer ScrollViewer { get; }
UserControl Instance { get; }
float CurrentZoom { get; }
void Load(IPdfSource source, string password = null);
void Zoom(double zoomFactor);
void ZoomIn();
void ZoomOut();
void ZoomToWidth();
void ZoomToHeight();
void GotoPage(int pageNumber);
void GotoPreviousPage();
void GotoNextPage();
int GetCurrentPageIndex(ViewType viewType);
}
The interface is implemented by the following classes:
internal partial class SinglePageMoonPdfPanel : UserControl, IMoonPdfPanel
{...}
internal partial class ContinuousMoonPdfPanel : UserControl, IMoonPdfPanel
{...}
The wrapper panel MoonPdfPanel
has only a reference to the common interface IMoonPdfPanel
. The MoonPdfPanel
delegates the actions, e.g. zoom or navigation, to the current instance of IMoonPdfPanel
. An example is shown in the code below.
public partial class MoonPdfPanel : UserControl
{
...
private IMoonPdfPanel innerPanel;
...
public void GotoNextPage()
{
this.innerPanel.GotoNextPage();
}
}
The XAML for the wrapper panel MoonPdfPanel
is shown below:
<UserControl x:Class="MoonPdfLib.MoonPdfPanel" ...>
<DockPanel LastChildFill="True" x:Name="pnlMain">
</DockPanel>
</UserControl>
As you can see, the XAML is plain simple. The user control only contains a DockPanel
where the appropriate user control (SinglePageMoonPdfPanel
or ContinuousMoonPdfPanel
) will be added to. This is shown below in the code behind the MoonPdfPanel
. Whenever the PageRowDisplayType
changes, the current innerPanel
is removed from the dockpanel (pnlMain
). Then a new instance is created (depending on the PageRowDisplayType
) and it is added to the dockpanel
.
this.pnlMain.Children.Clear();
if (pageRowDisplayType == PageRowDisplayType.SinglePageRow)
this.innerPanel = new SinglePageMoonPdfPanel(this);
else
this.innerPanel = new ContinuousMoonPdfPanel(this);
this.pnlMain.Children.Add(this.innerPanel.Instance);
Implementation (Data Binding and Virtualization)
As shown in an earlier XAML above, the data binding for the PDF pages has two properties ImageSource
and Margin
. These are part of the class PdfImage
(see below). The ImageSource
property is for holding the image of the PDF page and the Margin
property is for defining the margins of the PDF pages, i.e., the horizontal margin between two pages (when using ViewType.Facing
or ViewType.BookView
). For the Margin
, only the Right
-property of Thickness
is used, but I choose the Thickness
structure instead of a simple double
, because it makes data binding easier.
internal class PdfImage
{
public ImageSource ImageSource { get; set; }
public Thickness Margin { get; set; }
}
The logic for loading the required PDF pages is encapsulated in the class PdfImageProvider
. This class implements the generic IItemsProvider
interface from Paul's data virtualization solution. The two important methods of PdfImageProvider
are FetchCount
and FetchRange
(see below).
internal class PdfImageProvider : IItemsProvider<IEnumerable<PdfImage>>
{
...
public int FetchCount()
{
if (count == -1)
count = MuPdfWrapper.CountPages(pdfSource);
return count;
}
public IList<IEnumerable<PdfImage>> FetchRange(int startIndex, int count)
{
for(...)
{
using (var bmp = MuPdfWrapper.ExtractPage(pdfSource, i, this.Settings.ZoomFactor))
{
...
var bms = bmp.ToBitmapSource();
bms.Freeze();
var img = new PdfImage { ImageSource = bms, Margin = margin };
...
}
}
}
...
}
The first method is relevant for the data virtualization used in the ContinuousMoonPdfPanel
. It returns the number of the virtualized items. In our case, this is the number of pages in a PDF document. This number can be easily be determined with the help of the MuPdfWrapper
, which offers the CountPages
method.
The other method FetchRange
is for retrieving the PdfImage
s to display. The code above shows the call of the ExtractPage
method to get the bitmap of the respective PDF page. This bitmap is then converted to a BitmapSource
object, with help of the custom extension method ToBitmapSource
(not shown here). On this BitmapSource
object, we then call the Freeze
method, to make it unmodifiable. This is important, because for the ContinuousMoonPdfPanel
we use Paul's AsyncVirtualizingCollection
(see here), which calls the FetchRange
asynchronously on a different thread. Because the BitmapSource
object is not created on the UI-thread, the later binding (which happens on the UI-thread) would fail, if we would not call the Freeze
method.
After the above steps, a new PdfImage
object is created and populated with the according values. The FetchRange
method returns a list of page rows, where one page row is expressed as an IEnumerable<PdfImage>
object. This list is used later for data binding (see below).
When the FetchRange
method is called from the SinglePageMoonPdfPanel
, only the first item of the list (the first page row) is needed, because this panel only shows one page row at a time. This looks like this:
this.itemsControl.ItemsSource = this.imageProvider.FetchRange
(startIndex, this.parent.GetPagesPerRow()).FirstOrDefault();
You can see from the method call of FirstOrDefault
, that we are only interested in the first item of the result list, which is an object of type IEnumerable<PdfImage>
.
When we are using the ContinuousMoonPdfPanel
, we are not explicitly calling the FetchRange
method. Instead we are making use of the generic AsyncVirtualizingCollection
from Paul's article. This class manages the virtualization. It expects an object of IItemsProvider
, which we provide with an object of PdfImageProvider
. The FetchRange
method (which is part of the IItemsProvider
interface), will be called implicitly by the AsyncVirtualizingCollection
object whenever new items are requested (this happens for example when the user scrolls through the document). The following line shows how the items source is assigned in the ContinuousMoonPdfPanel
.
this.itemsControl.ItemsSource = new AsyncVirtualizingCollection<IEnumerable<PdfImage>>
(this.imageProvider, this.parent.GetPagesPerRow(), pageTimeout);
One important part to make the virtualization work, is the CustomVirtualizingPanel
that was mentioned above. Here is an excerpt that shows the relevant part of above XAML. The first bold text shows the XML namespace to reference the clr namespace, to include the user control. The second bold text shows that the CustomVirtualizingPanel
is used as the ItemsPanel
of our ItemsControl
.
<UserControl x:Class="MoonPdfLib.ContinuousMoonPdfPanel"
xmlns:virt="clr-namespace:MoonPdfLib.Virtualizing" ...>
...
<ItemsControl Name="itemsControl">
...
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<virt:CustomVirtualizingPanel />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
</ItemsControl>
</UserControl>
Because the items are virtualized, the CustomVirtualizingPanel
needs to know how much space is required by the items, i.e. what is the max width and total height of all the virtualized items. This is needed for a correct behaviour of the scrollviewer. The first step to calculate the required space was to get the bounds of all PDF pages for a given document. This was done through the MuPdfWrapper
, which uses the native BoundPage
methode, which gives us a Rectangle
for a given PDF page. The method below gets all the page bounds as Size[]
. It also takes a possible rotation of the pages into account. If no rotation or an 180 degree rotation is specified, we need not change anything. But otherwise we switch the width and height of the bounds (see bold text). This was achieved here via the delegate sizeCallback
.
public static System.Windows.Size[] GetPageBounds(IPdfSource source,
ImageRotation rotation = ImageRotation.None)
{
Func<double, double, System.Windows.Size> sizeCallback =
(width, height) => new System.Windows.Size(width, height);
if( rotation == ImageRotation.Rotate90 || rotation == ImageRotation.Rotate270 )
{
sizeCallback = (width, height) =>
new System.Windows.Size(height, width);
}
using (var stream = new PdfFileStream(source))
{
var pageCount = NativeMethods.CountPages(stream.Document);
var resultBounds = new System.Windows.Size[pageCount];
for (int i = 0; i < pageCount; i++)
{
IntPtr p = NativeMethods.LoadPage(stream.Document, i);
Rectangle pageBound = NativeMethods.BoundPage(stream.Document, p);
resultBounds[i] = sizeCallback(pageBound.Width, pageBound.Height);
NativeMethods.FreePage(stream.Document, p);
}
return resultBounds;
}
}
But knowing the bounds of the PDF pages is only half the story, because we need to take into account, that it is possible to display two PDF pages in one page row, which would result in a different required space. So after knowing the bounds of the single PDF pages, we calculate the required space for all the page rows. This is done in the method CalculatePageRowBounds
(see below). The required width of a page row is the sum of the width of the relevant PDF pages plus the chosen horizontal margin between them. The required height of a page row is the maximum of the relevant PDF pages plus the chosen vertical margin.
Example: Let's say for example that the ViewType.Facing
(two PDF pages in a page row) is chosen and the size of both PDF pages is 600x800 pixel (width x height) and the horizontal and vertical margins are both 4 pixel. Then the calculated size of the page row would be 1204x1604 pixel. This calculation must be done for all page rows, because it is possible that some PDF pages are wider or higher than others.
private PageRowBound[] CalculatePageRowBounds(Size[] singlePageBounds, ViewType viewType)
{
var pagesPerRow = Math.Min(GetPagesPerRow(), singlePageBounds.Length);
var finalBounds = new List<PageRowBound>();
var verticalBorderOffset = (this.PageMargin.Top + this.PageMargin.Bottom);
if (viewType == MoonPdfLib.ViewType.SinglePage)
{
finalBounds.AddRange(singlePageBounds.Select(p => new PageRowBound(p,verticalBorderOffset,0)));
}
else
{
var horizontalBorderOffset = this.HorizontalMargin;
for (int i = 0; i < singlePageBounds.Length; i++)
{
if (i == 0 && viewType == MoonPdfLib.ViewType.BookView)
{
finalBounds.Add(new PageRowBound(singlePageBounds[0], verticalBorderOffset, 0));
continue;
}
var subset = singlePageBounds.Take(i, pagesPerRow).ToArray();
finalBounds.Add(new PageRowBound(new Size(subset.Sum(f => f.Width),
subset.Max(f => f.Height)), verticalBorderOffset,
horizontalBorderOffset * (subset.Length - 1)));
i += (pagesPerRow - 1);
}
}
return finalBounds.ToArray();
}
So we have finally calculated the required space for all the page rows. We assign these bounds to the PageRowBounds
property of the CustomVirtualizingPanel
object (see below). Based on these bounds, we can later determine the total required space for CustomVirtualizingPanel
. This is done in the CalculateExtent
method. We take the maximum width of all page rows, so we know how broad the extent must be. We then summarize the heights of all page rows to get the total height that is required.
internal class CustomVirtualizingPanel : VirtualizingPanel, IScrollInfo
{
...
public Size[] PageRowBounds { get; set; }
private System.Windows.Size CalculateExtent(...)
{
...
var maxWidth = PageRowBounds.Select(f => f.Width).Max();
var totalHeight = PageRowBounds.Sum(f => f.Height);
return new Size(maxWidth, totalHeight);
}
...
}
Including MoonPdfPanel in your Application
Including the MoonPdfPanel
in your application is easy. The binaries for MoonPdfLib
contain three DLL files. Two of them are .NET assemblies (MoonPdfLib.dll and MouseKeyboardActivityMonitor.dll
). The other DLL (libmupdf.dll) is a native dll and contains the MuPdf functionality. First of all, you need to add a reference in your project to the MoonPdfLib.dll
, which contains the MoonPdfPanel
user control. Secondly, you need to make sure that the other two dlls are also in the same output folder as the referenced MoonPdfLib.dll.
The DLLs are now in place. You can now access and position the MoonPdfPanel
in your application. An example for this is shown in the xaml below. First of all, you need to include the xml namespace for the MoonPdfLib
assembly. This is the first bold text. Secondly, you can include the MoonPdfPanel
with the chosen xml namespace prefix (in our case mpp
). This is shown in the second bold text. The XAML below also shows some dependency properties that are set, for example the background color of the control or the view type.
<Window x:Class="YourApplication.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mpp="clr-namespace:MoonPdfLib;assembly=MoonPdfLib">
<DockPanel LastChildFill="True">
<mpp:MoonPdfPanel x:Name="moonPdfPanel"
Background="LightGray" ViewType="SinglePage"
PageDisplay="ContinuousPages"
PageMargin="0,2,4,2" AllowDrop="True" />
</DockPanel>
</Window>
A short notice for the properties PageMargin
and AllowDrop
. PageMargin
specifies the horizontal and vertical margins between PDF pages and page rows, respectively. The value for the Left
property is not used (see the value 0 above). The value for the Top
and Bottom
are for the vertical spaces before and after a page row. The value for Right
specifies the horizontal margin between PDF pages. The AllowDrop
property stems from the base class UIElement
. If it is set to True
, it is possible to drag and drop a PDF file into the MoonPdfPanel
and it will be automatically loaded.
With the MoonPdfPanel
in place, there are many public
methods that can be called. The most important one is the OpenFile
method, which loads a PDF based on a full file path. After that, you can call other methods like, ZoomIn
or GotoNextPage
. Many functionalities can also be access via mouse and keyboard, especially zooming and navigation functions. A good starting point to explore the MoonPdfPanel
is to look at the PDF viewer MoonPdf
. MoonPdf
includes the MoonPdfPanel
and creates a small user interface (main menu) to access the most important functions of the MoonPdfPanel
.
Handling password protected PDF files
MoonPdf version 0.2.3. added the ability to open password protected PDF files. For that to work, you can specify the password in the OpenFile
method of MoonPdfPanel
. There is also a callback event which makes it possible to ask for the password before opening the PDF file. This is for example necessary, when the user drags a file into the user control. The event is defined in MoonPdfPanel
and is called PasswordRequired
. An example is shown below (excerpt of MainWindow.xaml.cs
):
moonPdfPanel.PasswordRequired += moonPdfPanel_PasswordRequired;
void moonPdfPanel_PasswordRequired(object sender, PasswordRequiredEventArgs e)
{
var dlg = new PdfPasswordDialog();
if (dlg.ShowDialog() == true)
e.Password = dlg.Password;
else
e.Cancel = true;
}
Loading PDF documents from memory
The MoonPdfPanel
contains the following method to load PDF documents:
public void Open(IPdfSource source, string password = null)
{
...
}
To open a PDF from memory, you can use the MemorySource
class, for example like this:
MoonPdfPanel p = new MoonPdfPanel();
...
byte[] bytes = File.ReadAllBytes("somefilename.pdf");
var source = new MemorySource(bytes);
p.Open(source);
- Single page and continuous page display
- View single pages or multiple pages (facing or book view)
- Click and drag scrolling
- Navigation functionality, e.g. goto page, next page, etc.
- Zoom functionality, including "Fit to height" and "Fit to width"
- Rotate functionality
- Drop functionality (drop PDF files into the panel to open them)
- Open password protected PDF files
- Open PDF documents from memory (
byte[]
)
Final remarks
After lurking on CodeProject for almost 10 years (9 years and 8 month at the time of writing), I finally published my first article here ;-).
I hope you find the article and the MoonPdfPanel
useful. I appreciate any of your comments and suggestions. Maybe you can even use the MoonPdfPanel
in some of your projects, then I would be very interested to hear about it.
History
- 28 November 2013: Added the ability to open a PDF document from memory
byte[]
. Added new version 0.3.0. - 26 September 2013: Added the ability to open password protected PDF files. Added new version 0.2.3.
- 21 May 2013: Fixed an issue with custom scrollviewers. Added new version 0.2.2.
- 9 May 2013: Fixed an issue with non-standard DPI. Added new version 0.2.1.
- 18 April 2013: Article submitted.