Características no obvias de la aplicación Rotativa para generar PDF en la aplicación ASP.NET MVC

Muchos desarrolladores se enfrentan a la tarea de crear informes PDF para aplicaciones web, una solicitud completamente natural. Me gustaría llamar su atención sobre mi experiencia con esa tarea cuando uso la biblioteca Rotativa para generar informes. En mi opinión, esta es una de las bibliotecas más convenientes para tal propósito en su segmento, pero cuando la utilicé me encontré con varios puntos no obvios de los que quiero hablar.


Para ser completamente honesto, me gustaría compartir el conjunto de rastrillos que pisé en el proceso de integración de esta biblioteca, sin duda, rápido y muy conveniente.


En este artículo no abordaré el problema de elegir una biblioteca. Todos pueden tener sus propios motivos para usar esto o aquello. Elegí Rotativa, porque con costos de instalación mínimos, obtuvo todo lo necesario para cubrir los requisitos del cliente. Además de ella, probé tres o cuatro opciones más.


Declaración del problema.


Aplicación web en ASP.NET MVC, .NET versión 4.6. Otras características no son relevantes en este contexto, con la excepción de la implementación. Se espera que la implementación se realice en Azure. Esto es importante porque algunas otras bibliotecas (por ejemplo, HiQPdf) no transfieren instalaciones en ciertas configuraciones de Azure, esto está documentado.


Necesito abrir un cierto informe HTML estático con un enlace, y una versión en PDF del mismo informe con el segundo enlace. El informe en sí mismo es simplemente un conjunto de algunas tablas, campos y gráficos para la demostración al usuario. Ambas versiones requieren un menú con navegación a través de secciones del informe, la presencia de tablas, algunos gráficos (colores, tamaño de texto, bordes).


Usando la Biblioteca Rotativa


Rotativa es tan fácil de aplicar como sea posible en mi opinión.


  1. Ya tiene un informe HTML listo en forma de una plantilla y controlador ASP.NET MVC, como este:

[HttpGet] public async Task<ActionResult> Index(int param1, string param2) { var model = await service.GetReportDataAsync(param1, param2); return View(model); } 

  1. Instalar el paquete Nuget Rotativa


  2. Agregar nuevo controlador para informe PDF



 [HttpGet] public async Task<ActionResult> Pdf(int param1, string param2) { var model = await service.GetReportDataAsync(param1, param2); return new ViewAsPdf("Index", model); } 

Esencialmente a partir de ahora, tiene un PDF devuelto como un archivo que contiene todos los datos del informe HTML original.


No describí el enrutamiento aquí, pero se entiende que ha configurado rutas para llamar correctamente a ambos controladores


Curiosamente, esta biblioteca en sí misma es esencialmente un contenedor sobre la conocida utilidad de consola wkhtmltopdf . La velocidad de trabajo en altitud, puede apostar en Azure, funcionará. Pero hay características de las que hablaremos.


Número de página


Es lógico suponer que el cliente imprimirá el PDF y desea ver el número de página. Aquí todo es extremadamente simple, gracias a los creadores de Rotativa.


De acuerdo con la documentación de Rotativa, a través del parámetro CustomSwitches , puede especificar los argumentos que se pasarán a la wkhtmltopdf utilidad wkhtmltopdf . Bueno, los consejos en línea son generosos con ejemplos. La siguiente llamada agrega un número al final de cada página:


 return new ViewAsPdf("Index", model) { PageMargins = new Rotativa.Options.Margins(10, 10, 10, 10), PageSize = Rotativa.Options.Size.A4, PageOrientation = Rotativa.Options.Orientation.Portrait, CustomSwitches = "--page-offset 0 --footer-center [page] --footer-font-size 8 }; 

Funciona muy bien El número de página en sí se pasa utilizando el parámetro [page] , este tipo de parámetros se reemplazará con valores específicos.


Además de [page] hay otros:
  • [página] Reemplazada por el número de páginas que se están imprimiendo actualmente
  • [desde la página] Reemplazado por el número de la primera página que se imprimirá
  • [topage] Reemplazado por el número de la última página a imprimir
  • [página web] Reemplazada por la URL de la página que se está imprimiendo
  • [sección] Reemplazado por el nombre de la sección actual
  • [subsección] Reemplazado por el nombre de la subsección actual
  • [fecha] Reemplazado por la fecha actual en formato local del sistema
  • [isodate] Reemplazado por la fecha actual en formato extendido ISO 8601
  • [hora] Sustituido por la hora actual en formato local del sistema
  • [título] Reemplazado por el título del objeto de la página actual
  • [doctitle] Reemplazado por el título del documento de salida
  • [página del sitio] Reemplazado por el número de la página en el sitio actual que se está convirtiendo
  • [páginas del sitio] Reemplazado por el número de páginas en el sitio actual que se está convirtiendo


Tabla de contenido


Los informes de varias páginas grandes requieren contenido PDF y navegación de página. Esto es muy conveniente y simplemente vital cuando el número de páginas en un informe excede de cien.


El manual de wkhtmltopdf contiene una lista completa de todas las opciones, entre las cuales se encuentra --toc . Al ver este parámetro, la utilidad esencialmente recopila todas las etiquetas <h1>, <h2>, ... <h6> en el documento y genera una tabla de contenido basada en ellas. En consecuencia, es necesario proporcionar el uso correcto de estas etiquetas de encabezado en su plantilla HTML.


Pero en realidad, agregar --toc no conlleva ninguna consecuencia. Como si no hubiera ningún parámetro. Sin embargo, otras opciones funcionan. Gracias a una publicación en algún foro, descubrí que este parámetro debería pasarse sin guiones: toc . De hecho, en este caso, el contenido se agrega como la primera página. Cuando hace clic en una línea en el contenido, va a la página deseada en el documento, los números de página son correctos.


Aún no está completamente claro cómo configurar los estilos, pero aún no lo he hecho.


Ejecución de Javascript


El siguiente punto que encontré fue la necesidad de agregar gráficos al informe. Mi página HTML contiene código JS que agrega gráficos usando la biblioteca dc.js Aquí hay un ejemplo:


 function initChart() { renderChart(@Html.Raw(Json.Encode(Model.Chart_1_Data)), 'chartDiv_1'); } function renderChart(data, chartElementId) { var colors = ['#03a9f4', '#67daff', '#8bc34a']; var barHeight = 45; var clientHeight = data.length * barHeight + 50; var clientWidth = document.getElementById(chartId).offsetWidth; var chart = dc.rowChart('#' + chartElementId); var ndx = crossfilter(dataToRender); var dimension = ndx.dimension(d => d.name); var group = dimension.group().reduceSum(d => d.value); chart .width(clientWidth) .height(clientHeight) .margins({ top: 16, right: 16, bottom: 16, left: 16 }) .ordinalColors(colors) .dimension(dimension) .group(group) .xAxis() .scale(d3.scaleLinear().domain([0, 2]).range([1, 3]).nice()); chart.render(); } 

Al mismo tiempo, en HTML, tengo un elemento correspondiente:


 <div id="chart_C2" class="dc-chart"></div> 

Para que este código funcione, debe importar las bibliotecas apropiadas: dc.js , d3.js , crossfilter.js . Una llamada a la función initChart creará un gráfico e insertará el resultado
svg al elemento especificado en el árbol.


Pero el PDF no contiene rastros de gráficos. Además de cualquier otro rastro de ejecución de código JavaScript antes de la representación de PDF. Verificar esto es bastante fácil: solo agregue el código elemental para crear un elemento <div> simple con texto, solo para probar el hecho de una llamada de JavaScript.


Empíricamente, resultó que la ubicación del código JS para wkhtmltopdf juega un papel importante. El código ubicado al final de <html> o, por ejemplo, al final de <body> JS no se ejecutará. Parece que la utilidad simplemente no lo nota, o no espera encontrarse con él allí.


Pero el código dentro del <head> se ejecuta. Por lo tanto, llegué al esquema cuando el código JavaScript se encuentra después de la declaración de estilos dentro de la <head> , y se llama por la construcción habitual:


 <body onload="initCharts()"> 

En este caso, el código se ejecutará como se esperaba.


Limitaciones de JavaScript


Pero todavía no había gráficos en el PDF de salida de todos modos. Luego comencé a adivinar que al no ser un navegador completo, el motor de ejecución y representación de PDF probablemente no sea perfecto y no entienda las últimas reglas. Nuevamente, experimentalmente, descubrí que las funciones de flecha no se perciben. Además, si el intérprete encuentra algo desconocido para él, simplemente deja de funcionar.


Reemplazar las funciones de flecha de la forma x => x.value con function(x) { return x.value; } más clásicas function(x) { return x.value; } function(x) { return x.value; } ayudó y se ejecutó todo el código, el gráfico resultante se metió en un archivo PDF.


Ancho del gráfico


Empíricamente, resultó que era necesario indicar claramente el ancho del elemento padre del gráfico. Para esto, especifiqué el estilo dc-chart . Contiene el ancho del gráfico en píxeles. De lo contrario, el gráfico en el PDF será muy pequeño, a pesar de que en la versión HTML ocupará todo el ancho. Especificar el ancho porcentual solo funcionará para HTML.


JavaScript / CSS en línea


En conclusión, me gustaría señalar que muchas bibliotecas que convierten HTML a PDF aceptan una cierta baseUrl como parámetro. Esta es la URL sobre la cual el convertidor completará las rutas relativas para recibir estilos CSS importados, archivos JavaScirpt o fuentes. No puedo decir con certeza cómo funciona esto en Rotativa, pero se me ocurrió un enfoque diferente.


Para acelerar la carga inicial del informe y eliminar el origen de los problemas de incrustación de scripts o archivos de estilo durante la conversión, incrusto los JS y CSS necesarios directamente en el cuerpo de la plantilla HTML.


Para hacer esto, cree los paquetes apropiados:


 public class BundleConfig { public static void RegisterBundles(BundleCollection bundles) { bundles.Add(new StyleBundle("~/Styles/report-html") .Include("~/Styles/report-common.css") .Include("~/Styles/report-html.css") ); bundles.Add(new StyleBundle("~/Styles/report-pdf") .Include("~/Styles/report-common.css") .Include("~/Styles/report-pdf.css") ); bundles.Add(new ScriptBundle("~/Scripts/charts") .Include("~/Scripts/d3/d3.js") .Include("~/Scripts/crossfilter/crossfilter.js") .Include("~/Scripts/dc/dc.js") ); } } 

Agregue una llamada de configuración para estos paquetes a Global.asax.cs


 protected void Application_Start() { ... BundleConfig.RegisterBundles(BundleTable.Bundles); } 

Y agregue el método apropiado para incrustar el código en la página. Debe colocarse en el mismo espacio de nombres que Global.asax.cs para que se pueda llamar al método desde la plantilla HTML:


 public static class HtmlHelperExtensions { public static IHtmlString InlineStyles(this HtmlHelper htmlHelper, string bundleVirtualPath) { string bundleContent = LoadBundleContent(htmlHelper.ViewContext.HttpContext, bundleVirtualPath); string htmlTag = $"<style rel=\"stylesheet\" type=\"text/css\">{bundleContent}</style>"; return new HtmlString(htmlTag); } public static IHtmlString InlineScripts(this HtmlHelper htmlHelper, string bundleVirtualPath) { string bundleContent = LoadBundleContent(htmlHelper.ViewContext.HttpContext, bundleVirtualPath); string htmlTag = $"<script type=\"text/javascript\">{bundleContent}</script>"; return new HtmlString(htmlTag); } private static string LoadBundleContent(HttpContextBase httpContext, string bundleVirtualPath) { var bundleContext = new BundleContext(httpContext, BundleTable.Bundles, bundleVirtualPath); var bundle = BundleTable.Bundles.Single(b => b.Path == bundleVirtualPath); var bundleResponse = bundle.GenerateBundleResponse(bundleContext); return bundleResponse.Content; } } 

Bueno, el toque final es una llamada de la plantilla:


 @Html.InlineStyles("~/Styles/report-pdf"); @Html.InlineScripts("~/Scripts/charts"); 

Como resultado, todos los CSS y JavaScript necesarios estarán directamente en HTML, aunque durante el desarrollo puede trabajar con archivos individuales.


Lo más probable es que muchos piensen inmediatamente en la ineficiencia de este enfoque en términos de solicitudes de almacenamiento en caché por parte del navegador. Pero tenía dos objetivos específicos:


  • para que el convertidor de PDF no tenga que hacer solicitudes en algún lugar para estilos o código, y el usuario tiene que esperar esto, respectivamente;
  • para que la primera descarga del informe PDF y HTML lleve un tiempo mínimo, sin la necesidad de esperar varias solicitudes adicionales. En el contexto de mi proyecto, esto es importante;

Saltos de página


La estructuración del informe en secciones puede estar acompañada de requisitos para comenzar una nueva sección desde una nueva página. En este caso, puede utilizar con éxito el enfoque CSS simple:


 .page-break-before { page-break-before: always; } .no-page-break-inside { page-break-before: auto; page-break-inside: avoid; } 

La utilidad wkhtmltopdf lee con éxito estas clases y entiende que es necesario comenzar una nueva página. La primera clase, page-break-before , le dice a la utilidad que siempre comience una nueva página con este elemento. La segunda clase, no-page-break-inside , debe aplicarse a aquellos elementos que es deseable mantener lo más completos posible en la página. Por ejemplo, tiene bloques consecutivos de información estructurada, o digamos tablas. Si caben dos bloques en la página, se ubicarán. Si el tercero no cabe en la página, no será el siguiente. Si es más grande que una página, entonces su transferencia ya es inevitable. Todo esto funciona de manera adecuada y conveniente.


Comportamiento flexible en wkhtmltopdf


Bueno, la última característica que noté está relacionada con el uso de estilos de marcado flexbox. Todos nos hemos acostumbrado a ellos y casi todo el marcado está hecho por flexiones. Sin embargo, wkhtmltopdf está un poco atrasado en este sentido. Las opciones de flexión horizontal no funcionan (al menos en mi caso, esto no funcionó. Vi en la red mencionar que vale la pena duplicar los estilos de flexión de la siguiente manera:


 display: -webkit-flex; display: flex; flex-direction: row; -webkit-flex-direction: row; -webkit-box-pack: justify; /* wkhtmltopdf uses this one */ -webkit-justify-content: space-between; justify-content: space-between; 

Pero desafortunadamente esto no condujo al marcado esperado en el PDF. Tuve que rehacer el diseño de algunos elementos para que la colocación horizontal de los bloques estuviera de acuerdo con los requisitos. Si alguien tiene experiencia exitosa integrando flexes para wkhtmltopdf, por favor comparta. Eso sería muy útil.


Algunos enlaces:


Source: https://habr.com/ru/post/es425511/


All Articles