Situation

All of you know that it is a relatively common occurrence to get a simple task that ends up taking up several days of coding. And then, when you finally hit a breakthrough and manage to implement it you look at the majestic 50 lines of code it took in the end and wonder what have you been doing all of that time.

The exact situation happened to me recently. After finishing up a feature for implementing gift cards for users, there was one additional tiny requirement - not only send the voucher in the form of an e-mail, but also attach it to the e-mail in the form of a PDF so the users can download and print it out. What was exactly so hard about it? Glad you asked.

Options

Disclaimer: Project in question uses .NET 6, your mileage may vary.

Of course there were multiple options to approach this problem. I tested out various different solutions as suggested in this APITemplate.io blog post. As the voucher was already present in the form of an e-mail template that is sent out via SendGrid, the obvious first approach was to try and convert the HTML and CSS from the template into a PDF on the backend. Most of those approaches rely on using a headless browser, e.g. via Puppeteer to render the HTML and then convert it to a PDF. None of those solutions worked good enough appearance-wise. In addition they are mostly not cross-platform so I hesitated on following through with them and locking myself into a Windows-only deployment. There were also difficulties with getting them to play along with Dockerization, as we use docker-compose for local development on this specific project.

So I turned over to the second option - use a template PDF for the base and then just fill in the text bits that are dynamic. There are multiple library options available for such operations, but most come with a hefty price tag, and even so I could not get many of them to work correctly. Finally, I reached a solution by using one of the older players on the market - iTextSharp.

iTextSharp

iTextSharp is a .NET port of the iText library created way back in 2000. It has served as the go to library for PDF files for years. The library was originally released under the GNU LGPL licence which means it could be used in proprietary software as long as the library itself isn't modified. Since then, the library has transitioned to an AGPL licence which mandates that your code needs to be open sourced, and as the project I'm working on is proprietary that was definitely not an option.

The last LGPL version of iText is 4.1.6 from 2009 but it is nevertheless still widely used because of the free licence. Of course, it lacks numerous features that have since been developed, so it depends on your use case if the features supported up until then will be enough, in my case it was more than enough.

Before being able to use the old version there remained the question of bringing such an old library up to speed for usage with modern .NET SDKs. Luckily there is a library developed by Vahid Nasiri called iTextSharp.LGPLv2.Core released under (as the name says) a LGPL licence, and it works perfectly. If you need Linux or container support, the readme has precise instructions on how to achieve it.

Finally, time for basic code setup

So, starting from a base PDF I first need to load it into memory. I will use a MemoryStream in conjunction with the PdfReader class from iTextSharp to load the file:

using (var memoryStream = new MemoryStream())
using (var reader = new PdfReader($"{Path.GetDirectoryName(AppContext.BaseDirectory)}/voucher.pdf"))
{
	//... code will go here, using statements will automatically dispose memoryStream and reader variables
}   

As for the file, I have put it into the project file structure where appropriate and then updated the .csproj of that specific project to include the following:

<ItemGroup>
    <None Update="relative_path_to_folder\*">
        <CopyToOutputDirectory>Always</CopyToOutputDirectory>
        <TargetPath>%(Filename)%(Extension)</TargetPath>
    </None>
</ItemGroup>

This code snippet will ensure the file gets copied over during build and subsequently deployed, or else the app will throw an exception upon trying to access it. The build action for the file is set to None. If you want to use your own font file, you can also include it in the folder (* picks up all files in the folder).

Handling the PDF

The main tool I need for actually editing the PDF is an instance of the PdfStamper class, which is in charge of 'adding extra content to an existing PDF document'.

var stamper = new PdfStamper(reader, memoryStream);

Next is picking a font, which depends if I want to use my own or a built-in one:

var font = BaseFont.CreateFont($"{Path.GetDirectoryName(AppContext.BaseDirectory)}/arial.ttf", BaseFont.IDENTITY_H, BaseFont.EMBEDDED);
// or
var font = BaseFont.CreateFont(BaseFont.HELVETICA_BOLD, BaseFont.CP1252, BaseFont.EMBEDDED);

Then I need to get a specific instance of a stamper for the page of the PDF I want to change, which in my case is the first one (it is not zero-indexed but one-indexed):

var firstPageStamper = stamper.GetOverContent(1);

In order to be able to position the text so it is horizontally centered, I will read the page size and calculate the center:

var pageSize = reader.GetPageSize(1);
var centerOfPage = (pageSize.Left + pageSize.Right) / 2; 

Now I can 'print' some text, however many times needed:

firstPageStamper.SetRgbColorFill(247, 228, 6);
firstPageStamper.BeginText();
firstPageStamper.SetFontAndSize(font, 10);
// Center of page is horizontal coordinate, number after that is vertical coordinate and last number is rotation
firstPageStamper.ShowTextAligned(Element.ALIGN_CENTER, "First text", centerOfPage, 385, 0);
firstPageStamper.EndText();

firstPageStamper.SetRgbColorFill(250, 250, 249);
firstPageStamper.BeginText();
firstPageStamper.SetFontAndSize(font, 18);
firstPageStamper.ShowTextAligned(Element.ALIGN_CENTER, "Second text", centerOfPage, 360, 0);
firstPageStamper.EndText();

After all manipulation is done, it is necessary to close the document and of course return the result. In my case I am attaching the PDF to an e-mail so I will convert it to a Base 64 string.

stamper.Close();

return Convert.ToBase64String(memoryStream.ToArray());

Recap

And that is it! For simple PDF manipulation in modern .NET, you can use iTextSharp.LGPLv2.Core. Here is the code example in its entirety:

public string GetPdf()
{
	// If using local file like here, don't forget to set build settings as explained for the file to get copied over for deployment!
	using (var memoryStream = new MemoryStream())
	using (var reader = new PdfReader($"{Path.GetDirectoryName(AppContext.BaseDirectory)}/voucher.pdf"))
	{
    	    var stamper = new PdfStamper(reader, memoryStream);

            var font = BaseFont.CreateFont($"{Path.GetDirectoryName(AppContext.BaseDirectory)}/arial.ttf", BaseFont.IDENTITY_H, BaseFont.EMBEDDED);

            var pageSize = reader.GetPageSize(1);
            var centerOfPage = (pageSize.Left + pageSize.Right) / 2;

            var firstPageStamper = stamper.GetOverContent(1);

            firstPageStamper.SetRgbColorFill(247, 228, 6);
            firstPageStamper.BeginText();
            firstPageStamper.SetFontAndSize(font, 10);
            // Center of page is horizontal coordinate, number after that is vertical coordinate and last number is rotation
            firstPageStamper.ShowTextAligned(Element.ALIGN_CENTER, "First text", centerOfPage, 385, 0);
            firstPageStamper.EndText();

            firstPageStamper.SetRgbColorFill(250, 250, 249);
            firstPageStamper.BeginText();
            firstPageStamper.SetFontAndSize(font, 18);
            firstPageStamper.ShowTextAligned(Element.ALIGN_CENTER, "Second text", centerOfPage, 360, 0);
            firstPageStamper.EndText();

            stamper.Close();

            return Convert.ToBase64String(memoryStream.ToArray());
	}
}    

There you have it, a free and simple option for editing your PDF files!