For this exercise, I only want to generate a PDF. No editing, reading or
password-protecting
PDFs required. The PDF content is a report consisting of table of data from a
database. The design makes use of Bootstrap 5 CSS and icons. I also want to use web fonts
(Open Sans from Google Fonts) within the PDF. Here's a screenshot of the web
version of the report. The table uses the table-striped
CSS class
to apply alternative backgrounds to table rows. Discontinued items are displayed using the text-black-50
class from bootstrap 5. It also uses Bootstrap icons to indicate whether items need to be reordered. The colour of the icon in these instances is controlled by the
text-danger
CSS class. The header and the logo are placed in a
flex container
and positioned using the justify-content-between
CSS
class from Bootstrap. You can
see the source code on Github if you are interested.
I decided to have a look at three different options: iText 7 - an up-to-date replacement for the iTextSharp library that I'm familiar with; DinkToPdf - a free open source project that does well in searches relating to PDF from HTML in ASP.NET Core; and ChromeHTMLToPdf - another free open source option.
Each option can generate a PDF file from a string of HTML (as well as other sources, including variously files, streams and URLs). I'm generating my HTML by rendering Razor partials to a string using the technique I blogged about previously. The contents of the partial is essentially a complete HTML5 file. It includes references to local CSS assets using relative paths:
<link href="/lib/bootstrap/dist/css/bootstrap.min.css" rel="stylesheet" type="text/css" /> <link href="/css/bootstrap-icons.css" rel="stylesheet" type="text/css" /> <link href="/css/pdf.css" rel="stylesheet" type="text/css" />
The first two references bring in Bootstrap 5 and Bootstrap icons, while the final one brings in some rules that set the font to Open Sans and ensure that tables are broken nicely over multiple pages.
iText
I should start by mentioning that, like the other two options, iText is also open source software. However, unlike the other two options, it is not free for commercial use. I have no idea what the cost of a commercial licence is. Their site requires that you fill out a form and have a sales person contact you to "establish the best licencing model for you". Note that you can use iText free of charge under the AGPL licence.
I'll show the full code for the PageModel class for the Razor page that
produces
the PDF generated by iText. It includes the code for getting the data for
the partial and for rendering it to a string. The services for both of these
tasks (IProductManager
and IRazorTemplateRenderer
) are
registered with the service container as scoped services (because they both
depend on other scoped services) and injected into the constructor. The
PageModel also includes a string property called BaseHref
which consists of the current request's Scheme
and Host
properties, resulting in an absolute URL. This is
important for iText.
public class ITextVersionModel : PageModel { private readonly IProductManager productManager; private readonly IRazorTemplateRenderer renderer; public ITextVersionModel(IProductManager productManager, IRazorTemplateRenderer renderer) { this.productManager = productManager; this.renderer = renderer; } string BaseHref => $"{HttpContext.Request.Scheme}://{HttpContext.Request.Host}"; public List<Product> Products { get; set; } = new(); public async Task<FileResult> OnGetReportFromPartialAsync() { Products = await productManager.GetProducts(); var html = await renderer.RenderPartialToStringAsync("_ProductReport-v3", this); ConverterProperties converterProperties = new (); converterProperties.SetBaseUri(BaseHref); using var stream = new MemoryStream(); HtmlConverter.ConvertToPdf(html, stream, converterProperties); return File(stream.ToArray(), MediaTypeNames.Application.Pdf, "Reorder Report (iText).pdf"); } }
The primary method for generating a PDF from HTML in iText is the
HtmlConverter.ConvertToPdf
method. This overload take a string, a stream
for the output and a ConverterProperties
object that consists of
options for the converter. You can tell this library was written by Java
developers. They love to provide methods for setting property values whereas
.NET developers are more likely to allow you to just set the value via a public
property. Otherwise the API for generating an A4 portrait PDF (the default
document size and orientation) is straightforward . Anyway, we use the
ConverterProperties
object to set the BaseUri
without which
iText is unable to resolve the relative URLs in the CSS references in the
partial, resulting in no styling being applied to the final PDF. I only
discovered this from a Stackoverflow post. The one example on the iText site
that demonstrates generating a PDF from HTML fails to mention it. Let's take a
look at what is actually generated. The resulting file size is 43KB:
The image, CSS and icons were all located and applied, but there are some
shortcomings. iText claims to support flex, but clearly it had trouble with the
Bootstrap justify-content-between
class. The header and the logo should have been
flush with the start and the end of the container respectively. In addition,
iText has not applied the text-danger
class to the icons that
appear in the Reorder column. In Bootstrap 5, text-danger
uses
CSS custom properties:
.text-danger { --bs-text-opacity: 1; color: rgba(var(--bs-danger-rgb),var(--bs-text-opacity))!important; }
It appears that iText does not support these yet, which is further evidenced by the fact that the striped effect has not been applied to the table. This also relies on custom properties in Bootstrap 5.
iText Pros
- Fully supported (with a commercial licence)
- In active development
- Includes support for advanced PDF features including editing, reading, forms, security
- Product developers seem reasonably active on Stackoverflow
- No third party dependencies
- Reasonably straightforward API for simple requirements
iText Cons
- Only free under the AGPL Licence terms
- No indication of the cost of a commercial licence
- Only partial support for more modern CSS features
- Website doesn't contain much by way of tutorials or guides
DinkToPdf
As I mentioned, DinkToPdf comes up in searches for generating PDFs in ASP.NET, which is why I look at it. The first thing to mention, however, is that there has been no new release since April 2017 and Github issues go unanswered, so this looks like a dead project. When you dig further, you find that it depends on the wkhtmltopdf library, which is also a dead project. This in turn depends on QtWebKit which has had no updates since 2012. Nevertheless, it is available, free and relatively simple to use for generating PDFs from HTML. However, it requires that you deploy the wkhtmltopdf native library (Windows dll is about 40MB) as part of your application manually. The Nuget installation process does not do this for you, although a forked version of this project, Haukcode.WkHtmlToPdfDotNet does .
In a web application, the recommended way to generate PDFs using DinkToPdf is
to use the thread-safe SynchronizedConverter
. This is best registered as a
singleton service:
builder.Services.AddSingleton<IConverter>(provider => new SynchronizedConverter(new PdfTools()));
I inject this into a service that uses the converter to return a byte array:
public class PdfGenerator : IPdfGenerator { private readonly IConverter converter; public PdfGenerator(IConverter converter) => this.converter = converter; public byte[] Render(GlobalSettings globalSettings, ObjectSettings objectSettings) => converter.Convert(new HtmlToPdfDocument() { GlobalSettings = globalSettings, Objects = { objectSettings } }); }
This service is also registered as a singleton:
builder.Services.AddSingleton<IPdfGenerator, PdfGenerator>();
The PageModel that uses DinkToPdf to generate the PDF is shown here. This
time, along with the services required to get the data, render the partial to a
string and generate the PDF, I've injected the IWebHostEnvironment
service. This is required so I can obtain the value of the WebRootPath property,
which is the absolute file path to the wwwroot folder. Whereas iText
requires you to explicitly set the base URL for static assets, I could only get
DinkToPdf to pick up the CSS and images if I provide a full file path instead of
a URL. We'll see the changes needed to the partial to accommodate this in a minute. The converter wants some global settings
(page size, orientation etc) and some object settings (the content).
public class DinkToPdfVersionModel : PageModel { private readonly IProductManager productManager; private readonly IWebHostEnvironment environment; private readonly IRazorTemplateRenderer renderer; private readonly IPdfGenerator pdfGenerator; public DinkToPdfVersionModel( IProductManager productManager, IWebHostEnvironment environment, IRazorTemplateRenderer renderer, IPdfGenerator pdfGenerator) { this.productManager = productManager; this.environment = environment; this.renderer = renderer; this.pdfGenerator = pdfGenerator; } public List<Product> Products { get; set; } = new(); public string WebRootPath => environment.WebRootPath; public async Task<FileResult> OnGetReportFromPartialAsync() { Products = await productManager.GetProducts(); var html = await renderer.RenderPartialToStringAsync("_ProductReport-dink", this); var globalSettings = new GlobalSettings { Orientation = Orientation.Portrait, PaperSize = PaperKind.A4, }; var objectSettings = new ObjectSettings() { HtmlContent = html, }; return File(pdfGenerator.Render(globalSettings, objectSettings), MediaTypeNames.Application.Pdf, "Reorder Report (DinkToPDF).pdf"); } }
Here's the links in the partial for the CSS files. The image src
attribute
also uses a file path:
<link href="@System.IO.Path.Combine(Model.WebRootPath,"lib\\bootstrap\\dist\\css\\bootstrap.min.css")" rel="stylesheet" type="text/css" /> <link href="@System.IO.Path.Combine(Model.WebRootPath,"css\\bootstrap-icons.css")" rel="stylesheet" type="text/css" /> <link href="@System.IO.Path.Combine(Model.WebRootPath,"css\\pdf.css")" rel="stylesheet" type="text/css" />
Here's the rendered result which came in at 29KB, a 33% decrease on the iText version:
Unsurprisingly, given that this library's rendering engine is 10 years old, flex is not supported at all. Nor are custom properties. I found that a reasonable result can be obtained by downgrading to Bootstrap 3 and using older ways to control position. If you have spent much time downgrading your HTML to accommodate the desktop version of Outlook for mailers, this is a small price to pay.
DinkToPdf Pros
- Free and always will be
- Smaller final file than iText
- All you need can be deployed with your web application
- Easy API
DinkToPdf Cons
- Dead project, so no support or new features
- No support for modern CSS
- Minimal documentation
- No support for advanced PDF features such as reading, editing, securing, forms
ChromeHtmlToPdf
The ChromeHtmlToPdf library makes use of Chrome headless, basically the Chrome browser without a UI. This means that the Chrome browser needs to be installed on the server and your application requires access to it. Assuming that you can resolve these requirements, here's the PageModel code for the Chrome version:
public class ChromeVersionModel : PageModel { private readonly IProductManager productManager; private readonly IRazorTemplateRenderer renderer; public ChromeVersionModel(IProductManager productManager, IRazorTemplateRenderer renderer) { this.productManager = productManager; this.renderer = renderer; } public List<Product> Products { get; set; } = new(); public string BaseHref => $"{HttpContext.Request.Scheme}://{HttpContext.Request.Host}"; public async Task<FileResult> OnGetReportFromPartialAsync() { Products = await productManager.GetProducts(); var html = await renderer.RenderPartialToStringAsync("_ProductReport-chrome", this); var pageSettings = new PageSettings(ChromeHtmlToPdfLib.Enums.PaperFormat.A4); var stream = new MemoryStream(); using var converter = new Converter(); converter.ConvertToPdf(html, stream, pageSettings); return File(stream.ToArray(), MediaTypeNames.Application.Pdf, "Reorder Report (Chrome).pdf"); } }
Very similar stuff to the other examples. As with the iText version, a base URL is required, only this time, it needs to be set in the partial file itself. In addition we need to import the fonts and icons explicitly because they don't seem to resolve when the imports are placed in CSS files:
<base href="@Model.BaseHref" /> <style> @@import url('https://fonts.googleapis.com/css2?family=Open+Sans:wght@400&display=swap'); @@import url("https://cdn.jsdelivr.net/npm/[email protected]/font/bootstrap-icons.css"); </style>
We instantiate an instance of the
Converter
in a using
block so that it is disposed at the end of the handler. We pass the HTML, a stream for the output, and a
PageSettings
object containing basic PDF options to its ConvertToPdf
method. The resulting PDF looks like this:
As you can see, this is the closest we get to the web version. However, the resulting file size comes in at a whopping 333KB. That 7.5 times larger than iText and 11.5 times larger than DinkToPdf.
ChromeHtmlToPdf Pros
- Free and always will be
- Easy API
- Full support for modern CSS
ChromeHtmlToPdf Cons
- Requires Chrome to be installed on the target server
- Massive file size compared with alternatives
- No support for advanced PDF features such as reading, editing, securing, forms
- No technical support available
- Minimal documentation
Summary
I've taken a look at generating PDF files from HTML within an ASP.NET Core application using three different tools. Each has their different features and requirements. Hopefully this exploration will help you choose a suitable solution for your use. If not, there are a large number of other solutions, mostly 100% commercial, available.
All the code in this article is made available under the AGPL licence on Github.