Return PDF View from MVC Action with iTextSharp

In the previous post I demonstrated how Razor templates could be used for email generation in an MVC site. I wanted to extend the idea further and generate PDF documents from Razor templates in a similar way.

We already have the ability to fetch the resulting HTML string from a View and Model using the method outlined here. From there all we really need to do is generate the PDF document data; which using iTextSharp's xmlworker is also relatively straight forward.

public class HtmlToPdfRenderer
{
    public static byte[] Render(string html)
    {
        byte[] data;

        using (MemoryStream ms = new MemoryStream())
        {
            using (Document doc = new Document())
            {
                PdfWriter writer = PdfWriter.GetInstance(doc, ms);
                    
                doc.Open();

                using (var srHtml = new StringReader(html))
                {
                    XMLWorkerHelper.GetInstance().ParseXHtml(writer, doc, srHtml);
                }
            }

            data = ms.ToArray();
        }

        return data;
    }
}

We can then create a Custom Action Result to bring all the pieces together to generate the HTML and return the rendered PDF to the browser. The Custom Action Result could be fairly simple:

public class PdfActionResult : ActionResult
{
    protected string ViewName { get; set; }
    protected object Model { get; set; }
    protected string Filename { get; set; }

    public PdfActionResult(string viewName, object model, string filename = "")
    {
        this.ViewName = viewName;
        this.Model = model;
        this.Filename = filename;
    }

    public override void ExecuteResult(ControllerContext context)
    {
        if (context == null)
            throw new ArgumentNullException("context");

        // get rendered HTML from view
        string html = ViewRenderer.RenderView(ViewName, Model, context);

        // generate the PDF content from HTML
        byte[] content = HtmlToPdfRenderer.Render(html);

        HttpResponseBase response = context.HttpContext.Response;

        response.Clear();
        response.ClearContent();
        response.ClearHeaders();
        response.ContentType = "application/pdf";
        response.AppendHeader("Content-Disposition", $"{Filename}.pdf");
        response.BinaryWrite(content);
        response.OutputStream.Flush();
        response.OutputStream.Close();
        response.End();
    }
}

We specifically write the binary data rather than passing the stream directly to avoid issues with corrupt PDF document downloads which can happen when passing streams around. With all the pieces in place within your controller you can now return a PdfActionResult instead of a normal Razor View:

return new PdfActionResult("~/Views/Reports/MyPdfReportView.cshtml", myReportViewModel, "reportfilename");

As usual this is not a complete solution, it relies on the resulting HTML from the template being valid XHTML. So you might not be able to simply return an existing View intended for screen display with modern html 5 and external resources as a PDF. Formatting a PDF document correctly usually involves fixed widths and specific spacing which you would not normally do in a modern website.

If you wanted to do that then you could try wkhtmltopdf to generate the PDF instead, but you may run into some of the same formatting issues. This solution is more suited for when you want to generate a well formatted PDF report possibly with headers footers or watermarks and use the Razor templates as a convenient way to develop your formatting.