Introduction
For the fourth article in my PDF Splitter series, we will start working with PDF files in .NET MAUI. Helpfully, MAUI contains a lot of useful PDF tools for reading PDF files out of the box, so we do not need to use any additional libraries for now.
By the end of this article we will be able to:
Open a PDF file
Display a list of pages
Select and view an individual page
Handling Pages
Before we can think about opening a PDF file, the first thing we need to do is create somewhere to store information about each page within that file so we can track the status of each page.
To do this we create a new class to represent a page which I've added into my 'models' directory. This class will initially allow us to maintain some of the state of the page within our application, for example, if the page is selected and also return a 'name' we can bind to when displaying the page.
public class PdfPageItem
{
public PdfPage Page {get;set;}
public int PageNumber { get; set; }
public string PageName { get { return $"Page {PageNumber}"; } }
public bool Selected { get; set; }
}
This class includes a PdfPage reference, which can be found in the Windows.Data.Pdf namespace.
Working with RandomAccessStreams
Now that we have a representation of our page, we want to be able to take this further and be able to display a small preview of our page and a bigger view of our page when we click on the preview.
To do this in MAUI we can render our PDF file 'on-demand' into an InMemoryRandomAccessStream and then set our ImageSource content to the stream, which is what's used by the UI to 'binds' to our image:
public class PdfPageItem
{
//.. Earlier items omitted for brevity
public ImageSource Preview => ImageSource.FromStream(() => Task.Run(GeneratePreview).Result.AsStream());
public ImageSource PageView => ImageSource.FromStream(() => Task.Run(GenerateView).Result.AsStream());
public async Task<InMemoryRandomAccessStream> GeneratePreview()
{
double resizeFactor = Page.Size.Height / Page.Size.Width;
double destinationHeight = 120;
double destinationWidth = destinationHeight / resizeFactor;
await Page.PreparePageAsync();
var stream = new InMemoryRandomAccessStream();
PdfPageRenderOptions pdfPageRenderOptions = new PdfPageRenderOptions();
pdfPageRenderOptions.DestinationHeight = (uint)destinationHeight;
pdfPageRenderOptions.DestinationWidth= (uint)destinationWidth;
await Page.RenderToStreamAsync(stream, pdfPageRenderOptions);
stream.Seek(0);
return stream;
}
public async Task<InMemoryRandomAccessStream> GenerateView()
{
await Page.PreparePageAsync();
var stream = new InMemoryRandomAccessStream();
await Page.RenderToStreamAsync(stream);
stream.Seek(0);
return stream;
}
}
To generate the main view we render the page as it is, but to generate the preview, you can see we 'resize' the page during rendering. This resizes will both reduce the size and subsequently the quality of the rendered image, so you may want to play around with the destination height to adjust this. I have found 120 seems to provide a nice balance.
Creating the PDF Service
Now that we have something to break our PDF Item into, we need to create the service to load the file.
The below code is the start of our service and accepts a pdf file path as a string, loads and breaks down the pdf file into its pages as individual page items and then finally stores those pages in an ObservableCollection which our UI can bind to later.
public class PdfService : IPdfService
{
private PdfDocument _document;
private string _filePath;
public PdfService()
{
Items = new ObservableCollection<PdfPageItem>();
}
public async Task LoadPdf(string path)
{
Items.Clear();
_filePath = path;
var file = await StorageFile.GetFileFromPathAsync(path);
_document = await PdfDocument.LoadFromFileAsync(file);
for(int i = 1; i <= _document.PageCount; i++)
{
Items.Add(new PdfPageItem
{
Page = _document.GetPage((uint)i-1),
PageNumber = i
}); ;
}
}
public ObservableCollection<PdfPageItem> Items { get; private set; }
}
Because the purpose of this PdfSplitter application is to allow us to select and extract pages into a new PDF file, we can then expand on this service to create a second collection to track selected pages and enable us to move pages between the two:
public class PdfService : IPdfService
{
private PdfDocument _document;
private string _filePath;
public PdfService()
{
Items = new ObservableCollection<PdfPageItem>();
SelectedItems = new ObservableCollection<PdfPageItem>();
}
public async Task LoadPdf(string path)
{
SelectedItems.Clear();
Items.Clear();
_filePath = path;
var file = await StorageFile.GetFileFromPathAsync(path);
_document = await PdfDocument.LoadFromFileAsync(file);
for(int i = 1; i <= _document.PageCount; i++)
{
Items.Add(new PdfPageItem
{
Page = _document.GetPage((uint)i-1),
PageNumber = i
}); ;
}
}
public void UnloadPdf()
{
Items.Clear();
SelectedItems.Clear();
_document = null;
}
public void SelectItem(PdfPageItem item)
{
SelectedItems.Add(item);
Items.Remove(item);
}
public void UnselectItem(PdfPageItem item)
{
SelectedItems.Remove(item);
Items.Add(item);
Items = new ObservableCollection<PdfPageItem>(Items.OrderBy(x => x.PageNumber));
}
public ObservableCollection<PdfPageItem> SelectedItems { get; private set; }
public ObservableCollection<PdfPageItem> Items { get; private set; }
public string PdfFilePath => _filePath;
}
I have also added the functionality 'unload' a PDF, should we wish to, along with creating an interface for our dependency injection container, in the MauiProgram.cs file:
public static MauiApp CreateMauiApp()
{
var builder = MauiApp.CreateBuilder();
builder
.UseMauiApp<App>()
.RegisterAppServices()
.RegisterViewModels()
.RegisterViews()
.ConfigureFonts(fonts =>
{
fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular");
fonts.AddFont("OpenSans-Semibold.ttf", "OpenSansSemibold");
fonts.AddFont("MaterialIcons-Regular.ttf", alias: "Icons");
});
return builder.Build();
}
public static MauiAppBuilder RegisterViews(this MauiAppBuilder builder)
{
//..
}
public static MauiAppBuilder RegisterViewModels(this MauiAppBuilder builder)
{
//..
public static MauiAppBuilder RegisterAppServices(this MauiAppBuilder builder)
{
builder.Services.AddSingleton<IPdfService, PdfService>();
return builder;
}
Now that we have the PDF service ready to be used, in part 2, we update our UI to allow us to open, and work with a PDF file.