Generování PDF pomocí Razor Engine

12.04.2021

Občas je potřeba vygenerovat PDF soubor a to je téma, kterému se věnuje tento článek.

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é si nainstalujeme Select.HtmlToPdf Nuget balíček. Vytvoříme si pohled (view), který budeme chtít převést do PDF. Dále potřebujeme třídu, která nám takový pohled převede na HTML a také třídu, která nám HTML již převede do výsledného PDF formátu. Výhoda při použití Razor šablony (view) je taková, že šabloně můžeme předat model s daty z kontroleru a tí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

Vytvoření třídy pro konverzi Razor šablony na HTML

Třída je univerzální a můžeme jí například použít i při generování obsahu emailu 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();
            }
        }
    }
}

Vytvoření třídy pro konverzi HTML na PDF

Tato třída funguje jako jednoduchý wrapper nad vybranou PDF knihovnou, která nám konverzi 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 zaregistrovat naše služby

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, tak obrázek nastavíme, aby se nám zkopíroval do output složky. Převést si obrázek na Base64 formát je jedna z možností, jak obrázek dostat do PDF.

Pdf.cshtml

@model Web.Models.PdfViewModel

<h1>Hello World!</h1>

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

Služby pak 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 jednoduchou cestu, jak z webové aplikace vygenerovat PDF dokument. Vytvořili jsme si třídu ViewRenderService, která má univerzální použití a která nám připraví HTML kód z Razor šablony a vytvořili jsme si třídu PdfService, která funguje jako wrapper nad PDF knihovnou našeho výběru. Obě třídy mají interface, takže je můžeme snadno použít v unit testech.

Odkazy