How to Generate PDF From HTML in .NET Core Applications

12 Feb 2021
|
6 min read
Behind The Code

No matter what kind of application you are creating. It can be a small or enterprise application: at some point, you will benefit from generating PDF files. The most convenient way to do it would be to create HTML templates and then convert them into PDF. But, generating PDF from HTML in .NET Core applications is not straightforward.

The Problem: Generating PDF From HTML in .NET

You would like to create the PDF, but most of the libraries are paid, and they cost A LOT.

Also, when you manage to find a free version or open-source version of a library, it is not supported anymore, or it does not work the way you expect.

The Solution: Puppeteer Sharp

This is the moment you have been waiting for. Let me introduce you to the Puppeteer Sharp.

Wait what… haven’t I seen this in the Javascript world?

Yes! That’s right. The Puppeteer comes from the NodeJS world, and the Puppeteer Sharp is a port to .NET.

Want specialists to implement the solution for you?

Let’s talk and discuss how we can help you.

Contact us

Generating the PDF

First, we have to install the Puppeteer Sharp Nuget Package in our project. 

After doing that, within a few lines of code, we can convert our HTML to PDF. Here is an ASP.NET Core Web API Action Example

ASP.NET Core Web API Action Example

[HttpGet]
public async Task<IActionResult> Print()
{
   await new BrowserFetcher().DownloadAsync(BrowserFetcher.DefaultRevision);
   await using var browser = await Puppeteer.LaunchAsync(new LaunchOptions
   {
       Headless = true
   });
   await using var page = await browser.NewPageAsync();
   await page.EmulateMediaTypeAsync(MediaType.Screen);
   await page.SetContentAsync("<div><h1>Hello PDF world!</h1><h2 style='color: red; text-align: center;'>Greetings from <i>HTML</i> world</h2></div>");
   var pdfContent = await page.PdfStreamAsync(new PdfOptions
   {
       Format = PaperFormat.A4,
       PrintBackground = true
   });
   return File(pdfContent, "application/pdf", "converted.pdf");
}

This is it. After running it, this is what we’ve got:

Isn’t that awesome?

Puppeteer Sharp has great documentation along with great support.

Don’t be afraid to check it out: Github Repository


Template Generator With ASP.NET Core

Wait a moment, you might say. You have mentioned that we can generate PDFs out of HTML templates!

Yeah, and right now I’m about to tell you how to do it with an ASP.NET Core application!

Basically, there are two ways. First, you can prepare HTML templates, and just ‘replace’ some values.

Or you can go even further and prepare a whole service that will use Razor Pages to generate raw HTML out of .cshtml files. So you can use models, viewbags, tempdata, and all stuff like that.

Let’s say that our application is going to be some kind of Web API with a possibility to generate razor views only for PDFs for now.

The first thing we want to do is provide some kind of service for generating templates.

RazorViewsTemplateService

Let’s create an interface for dependency injection:

Services/Meta/ITemplateService.cs
public interface ITemplateService
{
   Task<string> RenderAsync<TViewModel>(string templateFileName, TViewModel viewModel);
}

And its implementation:

public class RazorViewsTemplateService : ITemplateService
{
   private IRazorViewEngine _viewEngine;
   private readonly IServiceProvider _serviceProvider;
   private readonly ITempDataProvider _tempDataProvider;
   private readonly ILogger<RazorViewsTemplateService> _logger;

   public RazorViewsTemplateService(IRazorViewEngine viewEngine, IServiceProvider serviceProvider, ITempDataProvider tempDataProvider, ILogger<RazorViewsTemplateService> logger)
   {
       _viewEngine = viewEngine;
       _serviceProvider = serviceProvider;
       _tempDataProvider = tempDataProvider;
       _logger = logger ?? throw new ArgumentNullException(nameof(logger));
   }

   public async Task<string> RenderAsync<TViewModel>(string filename, TViewModel viewModel)
   {
       var httpContext = new DefaultHttpContext
       {
           RequestServices = _serviceProvider
       };

       var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor());

       await using var outputWriter = new StringWriter();
       var viewResult = _viewEngine.FindView(actionContext, filename, false);
       var viewDictionary = new ViewDataDictionary<TViewModel>(new EmptyModelMetadataProvider(), new ModelStateDictionary())
       {
           Model = viewModel
       };

       var tempDataDictionary = new TempDataDictionary(httpContext, _tempDataProvider);
       if (!viewResult.Success)
       {
           throw new KeyNotFoundException(
               $"Could not render the HTML, because {filename} template does not exist");
       }

       try
       {
           var viewContext = new ViewContext(actionContext, viewResult.View, viewDictionary,
               tempDataDictionary, outputWriter, new HtmlHelperOptions());

           await viewResult.View.RenderAsync(viewContext);
           return outputWriter.ToString();
       }
       catch (Exception ex)
       {
           _logger.LogError(ex, "Could not render the HTML because of an error");
           return string.Empty;
       }
   }
}

Great! We have our TemplateRenderer class, but we have to follow some best practices to make an application with good performance.

Prepare Puppeteer for Future Reuse

We have to prepare a Puppeteer Browser first.

Extensions/PuppeteerExtensions.cs
public static class PuppeteerExtensions
{
   private static string _executablePath;
   public static async Task PreparePuppeteerAsync(this IApplicationBuilder applicationBuilder,
       IWebHostEnvironment hostingEnvironment)
   {
       var downloadPath = Path.Join(hostingEnvironment.ContentRootPath, @"\puppeteer");
       var browserOptions = new BrowserFetcherOptions {Path = downloadPath};
       var browserFetcher = new BrowserFetcher(browserOptions);
       _executablePath = browserFetcher.GetExecutablePath(BrowserFetcher.DefaultRevision);
       await browserFetcher.DownloadAsync(BrowserFetcher.DefaultRevision);
   }

   public static string ExecutablePath => _executablePath;
}

Here, we just create an extension method for our Startup class, so that while the application starts, we can prepare browsers for generating the PDFs.

Configuration

Let’s configure our Startup, so we can use Razor Pages Views to generate the PDF and use our extension method!

Startup.cs
public void ConfigureServices(IServiceCollection services)
{
   services.AddControllersWithViews();
   services.AddScoped<ITemplateService, RazorViewsTemplateService>();
}

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
   if (env.IsDevelopment())
   {
       app.UseDeveloperExceptionPage();
   }

   app.UseRouting();

   app.PreparePuppeteerAsync(env).GetAwaiter().GetResult();
   app.UseEndpoints(endpoints => { endpoints.MapControllers(); });
}

Creating the template model

Then, after configuring our method, let’s create a template. Let’s say we would like to have an invoice template.

We would like to use different values of course, and we should create a ViewModel for this.

ViewModels/InvoiceViewModel.cs
public class InvoiceViewModel
{
   public DateTime CreatedAt { get; set; }
   public DateTime Due { get; set; }
   public int Id { get; set; }
   public string AddressLine { get; set; }
   public string City { get; set; }
   public string ZipCode { get; set; }
   public string CompanyName { get; set; }
   public string PaymentMethod { get; set; }
   public decimal Amount => Items.Sum(i => i.Amount);
   public ICollection<InvoiceItemViewModel> Items { get; set; }
}

public class InvoiceItemViewModel
{
   public string Name { get; set; }
   public decimal Amount { get; set; }

   public InvoiceItemViewModel(string name, decimal amount)
   {
       Name = name;
       Amount = amount;
   }
}

This will be a really simple invoice, so the model is not too complex.

Creating the Template View

We are going to use Razor Views for this functionality, so that every time the template is being used, it is going to have different values.

Views/Templates/InvoiceTemplate.cshtml
@model PdfGenerator.ViewModels.InvoiceViewModel

@{
   Layout = null;
}

<!doctype html>
<html>
<head>
   <meta charset="utf-8">
   <title>Invoice</title>
  
   <style>
   .invoice-box {
       max-width: 800px;
       margin: auto;
       padding: 30px;
       border: 1px solid #eee;
       box-shadow: 0 0 10px rgba(0, 0, 0, .15);
       font-size: 16px;
       line-height: 24px;
       font-family: 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif;
       color: #555;
   }
  
   .invoice-box table {
       width: 100%;
       line-height: inherit;
       text-align: left;
   }
  
   .invoice-box table td {
       padding: 5px;
       vertical-align: top;
   }
  
   .invoice-box table tr td:nth-child(2) {
       text-align: right;
   }
  
   .invoice-box table tr.top table td {
       padding-bottom: 20px;
   }
  
   .invoice-box table tr.top table td.title {
       font-size: 45px;
       line-height: 45px;
       color: #333;
   }
  
   .invoice-box table tr.information table td {
       padding-bottom: 40px;
   }
  
   .invoice-box table tr.heading td {
       background: #eee;
       border-bottom: 1px solid #ddd;
       font-weight: bold;
   }
  
   .invoice-box table tr.details td {
       padding-bottom: 20px;
   }
  
   .invoice-box table tr.item td{
       border-bottom: 1px solid #eee;
   }
  
   .invoice-box table tr.item.last td {
       border-bottom: none;
   }
  
   .invoice-box table tr.total td:nth-child(2) {
       border-top: 2px solid #eee;
       font-weight: bold;
   }
   </style>
</head>

<body>
   <div class="invoice-box">
       <table cellpadding="0" cellspacing="0">
           <tr class="top">
               <td colspan="2">
                   <table>
                       <tr>
                           <td class="title">
                               <img src="~/images/logo.png" style="width:100%; max-width:150px;">
                           </td>
                          
                           <td>
                               <b>Invoice #</b>@Model.Id<br>
                               <b>Created</b> @Model.CreatedAt.ToString("d")<br>
                               <b>Due</b> @Model.Due.ToString("d")
                           </td>
                       </tr>
                   </table>
               </td>
           </tr>
          
           <tr class="information">
               <td colspan="2">
                   <table>
                       <tr>
                           <td>
                               Happy Duck Co.<br>
                               Quack Street 21<br>
                               Duckyquacky, 01-111
                           </td>
                          
                           <td>
                               @Model.CompanyName<br>
                               @Model.AddressLine<br>
                               @Model.City, @Model.ZipCode
                           </td>
                       </tr>
                   </table>
               </td>
           </tr>
          
           <tr class="heading">
               <td>
                   Payment Method
               </td>
              
               <td>
                   Amount
               </td>
           </tr>
          
           <tr class="details">
               <td>
                   @Model.PaymentMethod
               </td>
              
               <td>
                   $@Model.Amount.ToString("F2")
               </td>
           </tr>
          
           <tr class="heading">
               <td>
                   Item
               </td>
              
               <td>
                   Price
               </td>
           </tr>
           @foreach (var item in Model.Items)
           {
               <tr class="item">
                   <td>
                       @item.Name
                   </td>
              
                   <td>
                      $@item.Amount.ToString("F2")
                   </td>
               </tr>
           }
          
           <tr class="total">
               <td></td>
              
               <td>
                  Total: $@Model.Amount
               </td>
           </tr>
       </table>
   </div>
</body>
</html>

Great. We have everything set up, now it’s time to generate the template!


Usage

But how to do it?

We have to prepare a simple controller with action which is going to use our previously created template service, and also Puppeteer to generate the PDF. 

First, we create a mock of InvoiceViewModel, so that we can have some dummy values for the purpose of this article. 

Then we use the previously created RazorViewsTemplateService through an injected interface to generate the HTML and then we pass it to the Puppeteer, which is responsible for generating the PDF.

After all these actions, we simply return the generated PDF file.

Controllers/PrintController.cs
[HttpGet]
   public async Task<IActionResult> Print()
   {
       var model = new InvoiceViewModel
       {
           CreatedAt = DateTime.Now,
           Due = DateTime.Now.AddDays(10),
           Id = 12533,
           AddressLine = "Jumpy St. 99",
           City = "Trampoline",
           ZipCode = "22-113",
           CompanyName = "Jumping Rabbit Co.",
           PaymentMethod = "Check",
           Items = new List<InvoiceItemViewModel>
           {
               new InvoiceItemViewModel("Website design", 621.99m),
               new InvoiceItemViewModel("Website creation", 1231.99m)
           }
       };
       var html = await _templateService.RenderAsync("Templates/InvoiceTemplate", model);
       await using var browser = await Puppeteer.LaunchAsync(new LaunchOptions
       {
           Headless = true,
           ExecutablePath = PuppeteerExtensions.ExecutablePath
       });
       await using var page = await browser.NewPageAsync();
       await page.EmulateMediaTypeAsync(MediaType.Screen);
       await page.SetContentAsync(html);
       var pdfContent = await page.PdfStreamAsync(new PdfOptions
       {
           Format = PaperFormat.A4,
           PrintBackground = true
       });
       return File(pdfContent, "application/pdf", $"Invoice-{model.Id}.pdf");
   }
}

Result

Let’s run the application, and Invoke the GET /api/print to see how it works. 

Voilà! Our template generator works, and it has the values which we set in the mocked ViewModel within controller action.


PDF From HTML Summary

Puppeteer Sharp is a great tool that has many functionalities including PDF generation. 

It is very well documented and comes along with great support. This is the tool that .NET Core developers have been waiting for. 

In combination with ASP.NET Core Razor Views, we can create powerful applications that can use Layouts, Components, and many more in the templates.

Resources

PDF
Software Development
HTML
.Net

Written by