Cómo crear un PDF a partir de una Vista en ASP.NET Core MVC

Desde la llegada a nuestras vidas de la nueva plataforma de Micorsoft ASP.NET Core, la creación de archivos PDF de forma dinámica en aplicaciones desarrolladas sobre este nuevo Framework, ha sido una tarea algo complicada en comparación con las anteriores versiones de ASP.NET.

Esto se debe a que la gran mayoría de las alternativas existentes para este propósito en el entorno ASP.NET MVC no son compatibles con el nuevo núcleo multiplataforma de .NET Core, y por otra parte, las que si lo son, requieren una gran cantidad de configuración y código, o son soluciones de pago bastante caras.

Buscando una solución a este problema, en este artículo veremos una forma muy simple y altamente configurable de crear dinámicamente archivos PDF sobre .NET Core (multiplataforma), a partir una Vista definida en nuestra aplicación ASP.NET Core MVC.

html to pdf

Para realizar este ejemplo, utilizaremos la herramienta Rotativa.AspNetCore. Esta librería de uso gratuito, podemos integrarla en nuestros proyectos ASP.NET Core MVC vía NuGet, y es compatible con versiones ASP.NET Core (>= 2.0.1) y .NET Standard 2.0.

Básicamente, Rotativa para .NET Core, crea archivos PDF de forma dinámica a partir de cualquier Vista existente en nuestra aplicación MVC. Esta funcionalidad la consigue con el uso de la herramienta wkhtmltopdf, que no es más que una librería de código abierto (LGPLv3) en línea de comandos, que transforma HTML e imágenes en PDF utilizando utilizando el motor de renderizado Qt Web Kit.

Lo interesante de la herramienta wkhtmltopdf es que es multiplataforma (Windows, Linux, MacOs), o sea, que correctamente integrada con Rotativa, se ajusta totalmente a la filosofía "cross-platform" de .NET Core.

wkhtmltopdf

Integrando Rotativa en nuestra aplicación ASP.NET Core MVC

En primer lugar, crearemos un nuevo proyecto Web MVC desde Visual Studio 2017, con plataforma de destino .NET Core 2.2 (para este ejemplo he utilizado .NET Core 2.2, pero son perfectamente válidas versiones >= 2.0.1).

La instalación de Rotativa la haremos a través de NuGet. Para esto abriremos la consola de administración de paquetes NuGet de Visual Studio (Herramientas > Administrador de paquetes NuGet > Consola del Administrador de paquetes), y ejecutamos el siguiente comando:

PM> Install-Package Rotativa.AspNetCore -Version 1.1.1

A continuación, obtendremos la herramienta wkhtmltopdf desde https://wkhtmltopdf.org/downloads.html.

Nota: Aunque Rotativa (.NET Core) y wkhtmltopdf son librerías multiplataforma, este ejemplo lo realizaremos únicamente para entornos Windows. Al final del artículo incluiré una serie de consideraciones finales, que pueden dar una cierta orientación a quienes quieran implementar esta solución para otros entornos como Linux, MacOs o incluso Docker.

En este caso, hemos descargado un instalador para Windows 64 bits, que instalará los archivos necesarios para nuestro proyecto (wkhtmltopdf.exe y wkhtmltoimage.exe) en el path por defecto C:\Program Files\wkhtmltopdf\bin\.

Como podemos ver, estos archivos son dos ejecutables (.exe) que deberán ser accesibles desde nuestro proyecto MVC, para que Rotativa pueda hacer uso de ellos al generar los PDFs.

Por defecto, la herramienta Rotativa buscará estos dos ejecutables (wkhtmltopdf.exe y wkhtmltoimage.exe) en la carpeta de nombre Rotativa ubicada en nuestro directorio raiz wwwroot\.

Personalmente, la idea de alojar archivos ejecutables en el directorio raiz de nuestra aplicación (donde se encuentra el contenido estático, imágenes, css, js, etc.) me parece una mala práctica, ya que estos pueden ser accesibles desde el exterior, o incluso puede dar problemas a la hora de publicar nuestra aplicación en algunos servicios de hosting en la nube.

Es por esto que para este ejemplo, alojaremos los ejecutables en un directorio creado en la raiz del proyecto .\Rotativa\Windows.

rotativa path

Importante: Para que estos dos ejecutables, sean accesibles cuando publiquemos nuestra aplicación en un entorno de producción, es necesario establecer su propiedad [Copiar en el directorio de salida] = Copiar siempre.

copiar siempre

A continuación, configuraremos Rotativa en el método Configure(IApplicationBuilder app, IHostingEnvironment env) de la clase Startup.cs, para indicarle donde se encuentran los archivos de la herramienta wkhtmltopdf.

   public void Configure(IApplicationBuilder app, IHostingEnvironment env)
   {
       // ...            

       app.UseMvc(routes =>
       {
            routes.MapRoute(
               name: "default",
               template: "{controller=Home}/{action=Index}/{id?}");
       });

       // Configuramos Rotativa indicándole el Path RELATIVO donde se
       // encuentran los archivos de la herramienta wkhtmltopdf.
       Rotativa.AspNetCore.RotativaConfiguration.Setup(env, "..\\Rotativa\\Windows\\");
   }

Construyendo nuestra aplicación ASP.NET Core MVC de ejemplo

El Modelo de datos

Para realizar este ejemplo, hemos utilizado el modelo de datos (tabla) Customers de la base de datos de pruebas Northwind de Microsoft. Como ORM utilizaremos Entity Framework Core.

Nota: Scripts disponibles para descargar al final del artículo

    public class Customer
    {
        [Key]
        [StringLength(5)]
        public string CustomerID { get; set; }

        [Required]
        [StringLength(40)]
        public string CompanyName { get; set; }

        [Required]
        [StringLength(30)]
        public string ContactName { get; set; }

        [StringLength(30)]
        public string ContactTitle { get; set; }

        [StringLength(100)]
        public string Address { get; set; }

        [StringLength(15)]
        public string City { get; set; }

        [StringLength(15)]
        public string Region { get; set; }

        [StringLength(10)]
        public string PostalCode { get; set; }

        [Required]
        [StringLength(15)]
        public string Country { get; set; }

        [StringLength(24)]
        public string Phone { get; set; }

        [StringLength(24)]
        public string Fax { get; set; }

        [Required]
        [StringLength(100)]
        [DataType(DataType.EmailAddress)]
        public string Email { get; set; }
    }

Nota: Como no es el objetivo de este artículo explicar como "enlazar" Entity Framework Core a una base de datos, supondremos que ya hemos definido el DbContext de la aplicación con el correspondiente DbSet del modelo de datos Customers en la clase AppDbContext.cs.

    public class AppDbContext : DbContext
    {
        // CONSTRUCTOR DE LA CLASE, QUE RECIBE POR INYECCIÓN DE DEPENDENCIAS
        // LAS OPCIONES DE CONFIGURACIÓN DEL 'DbContext' DE Entity Framework Core
        // Y SON PASADAS A LA CLASE BASE 'base(options)'.
        public AppDbContext(DbContextOptions<AppDbContext> options) : base(options)
        {

        }

        public DbSet<Customer> Customers { get; set; }
    }

Configuración del DbContext como servicio en la clase Startup.cs

    // This method gets called by the runtime. Use this method to add services to the 
    // container.
    public void ConfigureServices(IServiceCollection services)
    {
       services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);

       // REGISTRO DEL CONTEXTO DE DATOS COMO SERVICIO
       services.AddDbContext<AppDbContext>(options =>               
       options.UseSqlServer(Configuration.GetConnectionString("AppConnectionString")));
    }

Cadena de conexión en el archivo de configuración appsettings.json.

{
  "ConnectionStrings": {
    "AppConnectionString": "data source=*******;
     initial catalog=**********;user id=*********;password=*********"
  },

  "Logging": {
    "LogLevel": {
      "Default": "Warning"
    }
  },
  "AllowedHosts": "*"
}

La Vista

Crearemos una Vista de nombre ContactPDF.cshtml, que nos muestre los datos de contacto del Modelo Customers en formato de tabla, así como un logotipo corporativo y un título descriptivo.

@model IEnumerable<Customer>

@{
    ViewData["Title"] = "ContactPDF";
}
@{
    Layout = null;
}

<!DOCTYPE html>

<html>
<head>
    <meta name="viewport" content="width=device-width" />
    <title>ContactPDF</title>
    <link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.css" />
    <link rel="stylesheet" href="~/css/site.css" />
</head>
<body>

    <div class="row">
        <div class="col-sm-4">
            <img src="~/images/generic-logo.png" />
        </div>
        <div class="col-sm-8 text-right">
            <h2>Customers Contact</h2>
        </div>
    </div>

    <table class="table">
        <thead>
            <tr>
                <th>
                    @Html.DisplayNameFor(model => model.CompanyName)
                </th>
                <th>
                    @Html.DisplayNameFor(model => model.ContactName)
                </th>
                <th>
                    @Html.DisplayNameFor(model => model.Phone)
                </th>
                <th>
                    @Html.DisplayNameFor(model => model.Email)
                </th>
            </tr>
        </thead>
        <tbody>
            @foreach (var item in Model)
            {
                <tr>
                    <td>
                        @Html.DisplayFor(modelItem => item.CompanyName)
                    </td>
                    <td>
                        @Html.DisplayFor(modelItem => item.ContactName)
                    </td>
                    <td>
                        @Html.DisplayFor(modelItem => item.Phone)
                    </td>
                    <td>
                        @Html.DisplayFor(modelItem => item.Email)
                    </td>

                </tr>
            }
        </tbody>
    </table>


</body>
</html>

El Controlador 

Por último crearemos el Controlador CustomersController.cs, donde añadiremos una nueva Acción ContactPDF() que nos devuelva la Vista anteriormente creada, con los clientes y sus datos de contacto según el Modelo Customer.cs.

Nota: Resaltar que el contexto de datos AppDbContext, lo recibimos a través del constructor del Controlador mediante Inyección de Dependencias.

    public class CustomersController : Controller
    {
        private readonly AppDbContext _context;

        public CustomersController(AppDbContext context)
        {
            _context = context;
        }

        // GET: Customers/ContactPDF
        public async Task<IActionResult> ContactPDF()
        {
            return View(await _context.Customers.ToListAsync());
        }
       
        // ...
    }

Ejecutamos nuestra aplicación, y el resultado sería el siguiente:

customers-contact

Como vemos, la Vista ha funcionado correctamente y nos devuelve el resultado esperado. Ahora solo falta que este contenido HTML que hemos obtenido, nos sea devuelto al navegador en forma de archivo PDF.

Esto lo realizaremos de una manera extremadamente simple y a la vez elegante. Simplemente devolveremos desde la Acción ContactPDF(), un objeto de la clase ViewAsPdf (de la librería Rotativa.AspNetCore) el cual recibe como parámetros el nombre de la Vista y el Modelo de datos.

        // GET: Customers/ContactPDF
        public async Task<IActionResult> ContactPDF()
        {
            // return View(await _context.Customers.ToListAsync());
            return new ViewAsPdf("ContactPDF", await _context.Customers.ToListAsync())
            {
                 // ...
            };
        }

Ejecutamos de nuevo, y este es el resultado: un archivo PDF exactamente idéntico al renderizado del HTML anteriormente generado.

customers contact pdf

Configurando Rotativa.AspNetCore.ViewAsPdf

La clase ViewAsPdf de la librería Rotativa.AspNetCore, nos permite a través de sus propiedades, configurar múltiples características del archivo PDF generado dinámicamente. Aspectos como el nombre del archivo, la orientación, los márgenes, tamaño o números de página, son fácilmente modificables desde las propiedades del objeto ViewAsPdf.

viewaspdf config

Una de las propiedades más interesantes de la clase ViewAsPdf es CustomSwitches. A través de ella, podemos acceder directamente a la opciones de configuración en línea de comandos de la herramienta wkhtmltopdf.

Un ejemplo práctico de esta propiedad, es la posibilidad de asignar números de página a nuestro archivo PDF auto-generado:

  // GET: Customers/ContactPDF
  public async Task<IActionResult> ContactPDF()
  {
      return new ViewAsPdf("ContactPDF", await _context.Customers.ToListAsync())
      {
          // Establece el número de página.
          CustomSwitches = "--page-offset 0 --footer-center [page] --footer-font-size 12"
      };
  }

Existen infinidad de parámetros de configuración, tanto para wkhtmltopdf.exe como para wkhtmltoimage.exe.

Nota: Al final del artículo, están disponibles para descargar los archivos de ayuda para configuración en linea comandos de la herramientas wkhtmltopdf y wkhtmltoimage.

Añadiendo cabecera y pie de página personalizados a nuestro PDF

En ciertas ocasiones, podemos necesitar que nuestro PDF auto-generado, disponga de una cabecera y un pie personalizados en cada página del documento (por ejemplo una factura, un albarán etc.)

Para esto, y en primer lugar, debemos definir las plantillas Html que actuarán como cabecera y pie de página. Estas plantillas serán Vistas cshtml como las habituales en nuestro proyecto (no valen Vistas parciales), pudiendo recibir si fuera necesario un Modelo de datos desde el Controlador.

Para este ejemplo crearemos una vista ContactHeaderPDF.cshtml para la cabecera, con un logotipo corporativo y un título descriptivo.

@{
    Layout = null;
}

<!DOCTYPE html>

<html>
<head>
    <meta name="viewport" content="width=device-width" />
    <title>ContactHeaderPDF</title>
    <link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.css" />
    <link rel="stylesheet" href="~/css/site.css" />
</head>
<body>
    <div class="row">
        <div class="col-sm-4">
            <img src="~/images/generic-logo.png" />
        </div>
        <div class="col-sm-8 text-right">
            <h2>Customers Contact</h2>
        </div>
    </div>
</body>
</html>

También crearemos otra para el pie de página ContactFooterPDF.cshtml, que nos indicará el número de página del documento.

@{
    Layout = null;
}

<!DOCTYPE html>

<html>
<head>
    <meta name="viewport" content="width=device-width" />
    <title>ContactFooterPDF</title>
    <link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.css" />
    <link rel="stylesheet" href="~/css/site.css" />
</head>
<body>
    <div class="text-center">
        Página <span id='page'></span> de
        <span id='topage'></span>

        <script>
            var vars = {};
            var x = window.location.search.substring(1).split('&');
            for (var i in x) {
                var z = x[i].split('=', 2);
                vars[z[0]] = unescape(z[1]);
            }
            document.getElementById('page').innerHTML = vars.page;
            document.getElementById('topage').innerHTML = vars.topage;
        </script>

    </div>
</body>
</html>

Nota: Como vemos en la plantilla para el pie de página, hemos insertado un pequeño script que se encargará de analizar el QueryString de la petición de la página (window.location.search), donde Rotativa inserta como parámetros el número de página page y el total de páginas topage, entre otras variables informativas.

Seguidamente, crearemos las Acciones correspondientes para devolver cada Vista en el Controlador CustomersController.

    public IActionResult ContactHeaderPDF()
    {
        return View("ContactHeaderPDF");
    }

    public IActionResult ContactFooterPDF()
    {
        return View("ContactFooterPDF");
    }

Por último, modificaremos la Acción principal ContactPDF(), donde configuraremos la propiedad CustomSwitches de la clase ViewAsPdf, para que utilice las Vistas de cabecera y pie de página anteriormente creadas.

    // GET: Customers/ContactPDF
    public async Task<IActionResult> ContactPDF()
    {
        // Define la URL de la Cabecera 
        string _headerUrl = Url.Action("ContactHeaderPDF", "Customers", null, "https");
        // Define la URL del Pie de página
        string _footerUrl = Url.Action("ContactFooterPDF", "Customers", null, "https");

        return new ViewAsPdf("ContactPDF", await _context.Customers.ToListAsync())
        {
            // Establece la Cabecera y el Pie de página
            CustomSwitches = "--header-html " + _headerUrl + " --header-spacing 13 " +
                             "--footer-html " + _footerUrl + " --footer-spacing 0"
            ,
            PageMargins = new Margins(50, 10, 12, 10)
        };
    }

Nota: Como en todo proceso de "maquetación", no siempre quedarán ajustadas a la primera la nueva cabecera y pie de página al documento PDF generado. Para ello, debemos realizar nosotros este ajuste manualmente a través de los parámetros --header-spacing--footer-spacing de wkhtmltopdf, y la propiedad PageMargins de la clase ViewAsPdf.

Consideraciones finales

Como hemos visto en este artículo, es extremadamente sencillo generar archivos PDF de forma dinámica en entornos Windows, utilizando Rotativa para Asp.Net Core.

Teniendo en cuenta la característica multiplataforma de .NET Core, y la versatilidad de la herramienta wkhtmltopdf para ser utilizada en los tres sistemas operativos más importantes del mercado, sería de mucha utilidad saber como implementar este mismo ejemplo en un plataforma como Linux o Docker.

Una opción de sentido común, sería descargar las librerías para Linux de wkhtmltopdf, y alojarlas en un directorio relativo a la aplicación, al estilo /usr/bin/.

Rotativa.AspNetCore.RotativaConfiguration.Setup(env, "/usr/bin");

Sinceramente, no he probado este ejemplo en Linux, pero sí he buscado información en Internet y realmente no he encontrado mucha información esclarecedora acerca de esta cuestión:

https://stackoverflow.com/questions/53425561/how-to-use-the-rotativa-aspnetcore-package-in-linux-ubuntu-os-asp-net-core-2-1

https://github.com/webgio/Rotativa.AspNetCore/issues/7

Si realmente, instalar los archivos wkhtmltopdf correspondientes a cada sistema operativo en un directorio relativo, e indicar el path en la configuración de Rotativa en el archivo Startup.cs es realmente eficiente, la funcionalidad multiplataforma estaría solucionada.

Si este artículo te ha sido de interés, y dominas el desarrollo en entornos en Linux, espero tus comentarios acerca de la compatibilidad multiplataforma de Rotativa.AspNetCore y wkhtmltopdf en ASP.NET Core MVC.

Descargas

Script Tabla- dbo.Customers.sql
Datos de prueba - dbo.Customers.data.sql
help-wkhtmltopdf.html
help-wkhtmltoimage.html

   EtiquetasASP.NET Core .NET MVC PDF .NET Core

  Compartir


  Nuevo comentario

El campo Comentario es obligatorio.
El campo Nombre es obligatorio.

Enviando ...

  Comentarios

Ivette Ivette

Muy buen articulo me ha ayudado mucho en mi trabajo.
Rafael Acosta Administrador Rafael Acosta

@Ivette:

Gracias por tu comentario. Compartir los artículos en la redes sociales es fundamental para que este Blog siga ofreciendo publicaciones de calidad y en español.


David Soler David Soler

Es posible que la vista en pdf se abra en otra pestaña del navegador ?
Rafael Acosta Administrador Rafael Acosta

@David Soler:

Hola, simplemente debes crear un link en la página desde la cual quieras abrir la Vista en PDF, hacia la Acción ContactPDF del Controlador Customers, teniendo en cuenta que el atributo target= debe ser igual a "_blank".


Utilizando los Tag-Helpers de ASP.NET Core sería así:


<a asp-controller="Customers" asp-action="ContactPDF" target="_blank">Contactos en PDF</a>

 


Ricardo Marquez Ricardo Marquez

Excelente Articulo, consulta, donde puedo ver la documentación completa
Rafael Acosta Administrador Rafael Acosta

@Ricardo Marquez:


Gracias por tu comentario. Todo lo referente a Rotativa lo puedes consultar en el GitHub del desarrollador:  https://github.com/webgio/Rotativa.AspNetCore


 


 


Ricardo Marquez Ricardo Marquez

Una consulta, usando bootstrap en la vista pdf, no me respeta las columnas, sabe que puedo estar realizando mal?
Rafael Acosta Administrador Rafael Acosta

@Ricardo Marquez:

La verdad es que no sabría decirte el porqué te están fallando los estilos Bootstrap, ya que el ejemplo está realizado también con Bootstrap y funciona correctamente.


Intenta comprobar si es un problema de dimensiones horizontales en el diseño responsive (haciendo un resize en tu navegador), ya que debes tener en cuenta que vas a imprimir en DIN A4 y no son las mismas dimensiones que tu monitor.


Khalifa Khalifa

Gracias por etse gran articulo!
Llevo toda la tarde buscando, algo que pueda sustituir el itextSharp que estaba usando en aspNet, para otro proyecto de aspNet core, todo lo que encontré, no me convenció, con tanto código y tantas configuracion, hasta darme con este articulo. Que ya me ha dado una idea.
Me idea es crear un Pdf a partir de un parte de la pagina HTML. Creo que la idea seria crear tres vista (header, body y footer) body seria la parte especifica del documento html. header y footer, la cabecera y pie de pagina.
O hay algun otra forma de generar solo una parte del documnet html?

Gracias otra vez!
Rafael Acosta Administrador Rafael Acosta

@Khalifa:

Efectivamente, la idea que propones sería la correcta. Podrías crearte 3 Vistas (header, body y footer), y luego enviar solamente la Vista body.cshtml mediante ViewAsPdf.


return new ViewAsPdf("body", TuModelo);

También podrías incluir el header y el footer en la Vista común _Layout.cshtml, y tratarlo como se hace con las páginas maestras en ASP.NET MVC.


Juan Carlos Juan Carlos

Hola buenas queria saber que hacer porque tengo un problema al poner return new ViewAsPdf("ListaReparacionVehiculo", RepaSelec) ya sea RepaSelec una lista de objetos normal, pues tambien quiero mandar unos datos por medio de viewdata a la vista y no los lee, cuando hacia un return normal si lo hacia
Rafael Acosta Administrador Rafael Acosta

@Juan Carlos:

Según los creadores de Rotativa, en su GitHub indican que esta eventualidad ya la tienen solucionada en la última versión del producto https://github.com/webgio/Rotativa.AspNetCore/issues/3. Prueba a descargar la última versión y comprueba si te funciona.


Aun así, lo que puedes hacer es construirte un ViewModel a partir del Model que le pasas a la Vista, e incluir ahí la información que le pasas en ViewData. Luego le pasas el ViewModel a la Vista.


Rub&#233;n Katz Rubén Katz

Muchas gracias por tu post, vengo a comentar que si funciona correctamente en linux, en un Ubuntu 16.04.6 LTS (Xenial Xerus) bajando los paquetes amd64 y haciendo referencia a la carpeta:

Rotativa.AspNetCore.RotativaConfiguration.Setup(env, "/usr/local/bin");

Nota importante, realizar los siguientes pasos mencionados por yashvit en Github (https://github.com/webgio/Rotativa.AspNetCore/issues/7#issuecomment-455063816):


1.Download the .deb package from https://wkhtmltopdf.org/downloads.html
2.sudo dpkg -i <path to deb package>
3.Install missing dependencies using apt sudo apt -f install


Todo correcto, muchas gracias!!!
Rafael Acosta Administrador Rafael Acosta

@Rubén Katz:

Excelente! muchas gracias por tu comentario. Queda entonces demostrada la funcionalidad multi-plataforma de .NET Core.


Luis Luis

Excelente trabajo, solo tengo una duda cuando quiero plasmar en mi pdf un taba que procese unos datos cuenta con 20 columnas y quiero que las muestre completas como podría hacerlo en el caso de girar la hoja que la tome horizontal y no vertical como la toma

espero me puedas apoyar muchas gracias


saludos

Perfil para Rafael Acosta en Stack Overflow en español, preguntas y respuestas para programadores y profesionales de la informática.

  Etiquetas

.NET Core .NET Framework .NET MVC .NET Standard AJAX ASP.NET ASP.NET Core ASP.NET MVC Bootstrap Buenas prácticas C# Cookies Entity Framework Gráficos JavaScript jQuery JSON JWT PDF Pruebas Unitarias Seguridad SEO SOAP Sql Server SqLite Swagger Validación Web API Web Forms Web Services WYSIWYG

  Nuevos


  Populares















Utilizamos cookies propias y de terceros para mejorar nuestros servicios y ofrecerle una mejor experiencia de navegación. Si continúa navegando consideramos que acepta su uso. Más información   Acepto