Generování souborů ve formátu PDF pomocí Razor Engine

12.04.2021

Občas potřebujeme v aplikaci vygenerovat PDF soubor a to je téma dnešního článku.

Slovníček

ASP.NET Core Open-source webový framework od Microsoftu (první release 2016).
Select.HtmlToPdf Nuget balíček pro volné použití od společnosti SelectPDF (free varianta má limit 5 stránek).
FreeSpire.PDF Alternativou k Select.HtmlToPdf může být Nuget balíček pro volné použití od společnosti E-ICEBLUE (free varianta má limit 10 stránek).

Obsah

Úvod

Připravíme si webovou aplikaci, do které nainstalujeme NuGet balíček Select.HtmlToPdf. Vytvoříme pohled (view), který budeme chtít převést do souboru ve formátu PDF. Dále vytvoříme třídu, která nám tento pohled převede na HTML a také třídu, která nám HTML následně převede do výsledného PDF formátu. Výhodou použití Razor šablony (view) je, že šabloně můžeme předat model s daty z kontroleru, čímž můžeme dynamicky měnit obsah PDF souboru dle potřeby.

Příprava prostředí

Vytvoříme webový projekt ze základní šablony Visual Studia

dotnet new mvc --output Web

Pomocí Package Manager Console ve Visual Studiu nainstalujeme Select.HtmlToPdf.NetCore balíček

Install-Package Select.HtmlToPdf.NetCore

Převod Razor šablony na HTML

Třída je univerzální a můžeme ji například použít i při generování obsahu e-mailu z razor šablony apod.

IViewRenderService.cs

using System.Threading.Tasks;

namespace Web.Application.Services.ViewRender
{
    public interface IViewRenderService
    {
        Task<string> RenderToStringAsync(string viewName, object model);
    }
}

ViewRenderService.cs

using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Abstractions;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.Mvc.Razor;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
using Microsoft.AspNetCore.Routing;
using System;
using System.IO;
using System.Threading.Tasks;

namespace Web.Application.Services.ViewRender
{ 
    public class ViewRenderService : IViewRenderService
    {
        private readonly IRazorViewEngine _razorViewEngine;
        private readonly ITempDataProvider _tempDataProvider;
        private readonly IServiceProvider _serviceProvider;

        public ViewRenderService(IRazorViewEngine razorViewEngine,
            ITempDataProvider tempDataProvider,
            IServiceProvider serviceProvider)
        {
            _razorViewEngine = razorViewEngine;
            _tempDataProvider = tempDataProvider;
            _serviceProvider = serviceProvider;
        }

        public async Task<string> RenderToStringAsync(string viewName, object model)
        {
            var httpContext = new DefaultHttpContext { RequestServices = _serviceProvider };
            var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor());

            using (var sw = new StringWriter())
            {
                var viewResult = _razorViewEngine.FindView(actionContext, viewName, false);

                if (viewResult.View == null)
                {
                    throw new ArgumentNullException($"{viewName} does not match any available view.");
                }

                var viewDictionary = new ViewDataDictionary(new EmptyModelMetadataProvider(), new ModelStateDictionary())
                {
                    Model = model
                };

                var viewContext = new ViewContext(
                    actionContext,
                    viewResult.View,
                    viewDictionary,
                    new TempDataDictionary(actionContext.HttpContext, _tempDataProvider),
                    sw,
                    new HtmlHelperOptions()
                );

                await viewResult.View.RenderAsync(viewContext);

                return sw.ToString();
            }
        }
    }
}

Převod HTML na PDF

Tato třída funguje jako jednoduchý wrapper nad vybranou PDF knihovnou, která nám převod provede.

IPdfService.cs

namespace Web.Application.Services.Pdf
{
    public interface IPdfService
    {
        byte[] Create(string html);
    }
}

PdfService.cs (třída má již nadefinovanou konfiguraci pro layout, kterou lze v případě potřeby parametrizovat)

using SelectPdf;

namespace Web.Application.Services.Pdf
{
    public class PdfService : IPdfService
    {
        public byte[] Create(string html)
        {
            var converter = new HtmlToPdf();

            converter.Options.PdfPageSize = PdfPageSize.A4;
            converter.Options.PdfPageOrientation = PdfPageOrientation.Portrait;
            converter.Options.MarginLeft = 10;
            converter.Options.MarginRight = 10;
            converter.Options.MarginTop = 20;
            converter.Options.MarginBottom = 20;
            converter.Options.AllowContentHeightResize = false;
            converter.Options.AutoFitHeight = HtmlToPdfPageFitMode.NoAdjustment;
            converter.Options.AutoFitWidth = HtmlToPdfPageFitMode.NoAdjustment;

            PdfDocument doc = converter.ConvertHtmlString(html);

            byte[] result = doc.Save();

            doc.Close();

            return result;
        }
    }
}

Použití

Nyní máme již vše připravené a můžeme služby zaregistrovat

Startup.cs

public void ConfigureServices(IServiceCollection services)
{
    services.AddTransient<IViewRenderService, ViewRenderService>();

    services.AddTransient<IPdfService, PdfService>();

    services.AddControllersWithViews();
}

Připravíme si Model a Razor šablonu, kterou budeme chtít převést na PDF

PdfViewModel.cs

using System;
using System.IO;

namespace Web.Models
{
    public class PdfViewModel
    {
        public PdfViewModel()
        {
            // Load "files\image.jpg" as base64.
            ImageJpegBase64 =
                Convert.ToBase64String(
                    File.ReadAllBytes(
                        Path.Combine(
                            AppDomain.CurrentDomain.BaseDirectory, 
                                @"files\image.jpg")));
        }
        public string ImageJpegBase64 { get; private set; }
    }
}

Protože model obsahuje cestu k obrázku, nastavíme obrázek tak, aby se zkopíroval do projektové složky output. Jednou z možností, jak obrázek dostat do PDF, je převést ho na formát Base64.

Pdf.cshtml

@model Web.Models.PdfViewModel

<h1>Hello World!</h1>

<img src="data:image/jpeg;charset=utf-8;base64, @Model.ImageJpegBase64" />

Služby nyní můžeme načíst a použít kdekoliv v aplikaci, například v controlleru

using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using Web.Application.Services.Pdf;
using Web.Application.Services.ViewRender;
using Web.Models;

namespace Web.Controllers
{
    public class HomeController : Controller
    {
        private readonly ILogger<HomeController> _logger;
        private readonly IViewRenderService _viewRender;
        private readonly IPdfService _pdf;

        public HomeController(ILogger<HomeController> logger, IViewRenderService viewRender, IPdfService pdf)
        {
            _logger = logger;
            _viewRender = viewRender;
            _pdf = pdf;
        }

        public IActionResult DownloadPdf()
        {
            var model = new PdfViewModel();
            
            string html = _viewRender.RenderToStringAsync("Home/pdf", model).Result;
            
            byte[] pdf = _pdf.Create(html);

            return new FileContentResult(pdf, "application/pdf")
            {
                FileDownloadName = "Document.pdf"
            };
        }
    }
}

Při zavolání URL akce Home/DownloadPdf se nám PDF dokument stáhne

Závěr

V dnešním článku jsme si ukázali jednoduchý způsob, jak můžeme ve webové aplikaci generovat PDF soubory. Vytvořili jsme třídu ViewRenderService, která umí připravit HTML kód z Razor šablony a třídu PdfService, která funguje jako wrapper nad PDF knihovnou. Obě třídy mají vlastní rozhraní (interface), které můžeme využít v testech.

Odkazy