.NET MAUI - Saving PDF Files with ITextSharp

Photo by Onur Buz on Unsplash

.NET MAUI - Saving PDF Files with ITextSharp

·

4 min read

Introduction

In the earlier articles in my PDF Splitter series, we have relied on the inbuild .NET libraries to handle working with PDF files in MAUI, but these only support opening and displaying of PDF files. Now that we are looking to save our newly splitwe have to look eslewhere and in this article we will be using the LGPLv2 net core version of ITextSharp and the MAUI FileSaveViewer to write a PDF file to disk.

Add the Reference

The first thing we need to do is install the iTextSharp LGPLv2 package from nuget:

Install-Package iTextSharp.LGPLv2.Core

Creating a Save File Service

Then we add a new SavePdfService.cs file to our services directory.

To create our new PDF we need our service save function to accept three inputs:

  • The original PDF file path - As we are now working with a different library, we will need to open the original PDF file using that library.

  • A StorageFile - A StorgateFile object is what's returned from the inbuilt MAUI FileSavePicker and provides a stream for us to write our file to.

  • A list of page numbers to extract into our output file.

We can then open the original pdf using iTextSharps PdfReader and select the pages we would like to extract from the file.

We then use the pdfStamper to write those pages into a new MemoryStream, before resetting the position of the stream to the start, and copying the contents of our memory stream into our StorageFile stream:

public class SavePdfService : ISavePdfService
{
    public async Task SavePdf(string inputFile, StorageFile outputFile, IEnumerable<int> pages)
    {
        var stream = await outputFile.OpenAsync(FileAccessMode.ReadWrite);
        using var outputStream = stream.GetOutputStreamAt(0);

        var pdfStream = new MemoryStream();

        using var reader = new PdfReader(inputFile);
        reader.SelectPages(pages.ToArray());

        PdfStamper stamper = new PdfStamper(reader, pdfStream);
        stamper.Close();

        pdfStream.Position = 0;

        await pdfStream.CopyToAsync(outputStream.AsStreamForWrite((int)pdfStream.Length));

        stream.Dispose();            
    }
}

We then need to ensure we extract an interface of our service and register it in our MauiProgram.cs file alongside our existing PdfService:

public static MauiAppBuilder RegisterAppServices(this MauiAppBuilder builder)
{
    builder.Services.AddSingleton<IPdfService, PdfService>();
    builder.Services.AddSingleton<ISavePdfService, SavePdfService>();

    return builder;
}

Updating Our Views

Finally, we can hook up our save functionality by adding the OnSaveFileCommand and associated handler to our PdfViewPageViewModel:

public class PdfViewPageViewModel : BindableModelBase
{
    private IPdfService _pdfService;
    private ISavePdfService _savePdfService;
    private PdfPageItem _selectedPageItem;
    public ICommand OnItemSelectedCommand { get; private set; }
    public ICommand OnItemRemovedCommand { get; private set; }
    public ICommand OnSaveFileCommand { get; private set; }

    public PdfViewPageViewModel(IPdfService pdfService, ISavePdfService savePdfService)
    {
        _pdfService = pdfService;
        _savePdfService = savePdfService;
        OnItemSelectedCommand = new Command(SelectPage);
        OnItemRemovedCommand = new Command<string>(RemovePage);
        OnSaveFileCommand =  new Command<string>(async (s) => await OnSaveClicked(s));
    }

    //.. Code ommited for brevity

    private async Task OnSaveClicked(string s)
    {
        var window = new Microsoft.UI.Xaml.Window();
        var hwnd = WinRT.Interop.WindowNative.GetWindowHandle(window);

        FileSavePicker savePicker = new FileSavePicker();
        savePicker.SuggestedStartLocation = PickerLocationId.DocumentsLibrary;
        savePicker.FileTypeChoices.Add("PDF Document", new List<string>() { ".pdf" });
        savePicker.SuggestedFileName = $"New_Document_{DateTime.Now:ddMMyyyy_HHmmss}";

        WinRT.Interop.InitializeWithWindow.Initialize(savePicker, hwnd);
        var file = await savePicker.PickSaveFileAsync();

        await _savePdfService.SavePdf(_pdfService.PdfFilePath, file, _pdfService.SelectedItems.Select(x => x.PageNumber).ToList());

        await App.Current.MainPage.DisplayAlert("Success", "File has been saved", "OK");
        _pdfService.SelectedItems.Clear();
        NotifyPropertyChanged(() => DisplaySaveButton);
    }


    public bool DisplaySaveButton => _pdfService.SelectedItems.Any();
}

And adding our 'Save' button to our view by creating a PdfSaveControl:

<?xml version="1.0" encoding="utf-8" ?>
<ContentView xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:fonts="clr-namespace:PdfSplitter.Fonts"
             x:Name="this"
             x:Class="PdfSplitter.Controls.PdfSaveControl">
    <VerticalStackLayout BindingContext="{x:Reference this}">
        <Button  
            x:Name="Save"
            Margin="5"
            CornerRadius="10"
            BackgroundColor="DarkGreen"
            TextColor="White"
            FontFamily="Icons" 
            FontAttributes="Bold"
            FontSize="20"
            Text="{x:Static fonts:MaterialDesignRegular.Save}"
            Command="{Binding SaveCommand}"
            SemanticProperties.Hint="Save selected pages to new document."
            HorizontalOptions="End"
            VerticalOptions="End" />
    </VerticalStackLayout>
</ContentView>

Inserting the new control into the main PdfViewPage view in our final grid space:

<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:Controls="clr-namespace:PdfSplitter.Controls"
             x:Class="PdfSplitter.Views.PdfViewPage"             
             x:Name="pdfExtractorViewPage">

    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="4*" />
            <RowDefinition Height="auto" />
        </Grid.RowDefinitions>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="5*" />
            <ColumnDefinition Width="*" />
        </Grid.ColumnDefinitions>
        <Controls:PdfPageContent Grid.Row="0" Grid.Column="0" Page="{Binding SelectedPageItem}" SelectPageCommand="{Binding OnItemSelectedCommand}" />
        <Controls:PdfPages Grid.Row="0" Grid.Column="1" Grid.RowSpan="2" Pages="{Binding Items}" SelectedPage="{Binding SelectedPageItem, Mode=TwoWay}" />
        <Controls:PdfSelectedPages  Grid.Row="1" Grid.Column="0" SelectedPages="{Binding SelectedItems}" RemovePageCommand="{Binding OnItemRemovedCommand}" />
        <Controls:PdfSaveControl  Grid.Row="1" Grid.Column="0" SaveCommand="{Binding OnSaveFileCommand}" IsVisible="{Binding DisplaySaveButton}" VerticalOptions="End" />
    </Grid>
</ContentPage>

Conclusion

This concludes my series for creating a PDF Splitter application from scratch in .NET MAUI.

The full code is available on my GitHub here and may receive further updates and improvements as I continue to develop and enhance the application.

Thanks for reading!

Did you find this article valuable?

Support Dave K by becoming a sponsor. Any amount is appreciated!