Recursos não óbvios do aplicativo Rotativa para gerar PDF no aplicativo ASP.NET MVC

Muitos desenvolvedores enfrentam a tarefa de criar relatórios em PDF para aplicativos da Web, uma solicitação completamente natural. Gostaria de chamar a atenção para minha experiência com essa tarefa ao usar a biblioteca Rotativa para gerar relatórios. Essa é uma das bibliotecas mais convenientes para esse propósito em seu segmento, mas, ao usá-la, deparei com vários pontos não óbvios sobre os quais quero falar.


Para ser completamente honesto, gostaria de compartilhar o conjunto de rakes que pisei no processo de integração desta biblioteca, sem dúvida, rápido e muito conveniente.


Neste artigo, não abordarei a questão da escolha de uma biblioteca. Todos podem ter suas próprias razões para usar isso ou aquilo. Escolhi o Rotativa porque, com custos mínimos de instalação, ele tem tudo o que é necessário para atender às necessidades do cliente. Além dela, tentei mais três ou quatro opções.


Declaração do problema


Aplicativo da Web no ASP.NET MVC, .NET versão 4.6. Outros recursos não são relevantes neste contexto, com exceção da implantação. Espera-se que a implantação ocorra no Azure. Isso é importante porque algumas outras bibliotecas (por exemplo, HiQPdf) não transferem instalações em determinadas configurações do Azure, isso está documentado.


Preciso abrir um relatório HTML estático com um link e uma versão em PDF do mesmo relatório com o segundo link. O relatório em si é simplesmente um conjunto de algumas tabelas, campos e gráficos para demonstração ao usuário. Ambas as versões requerem um menu com navegação pelas seções do relatório, presença de tabelas, alguns gráficos (cores, tamanho do texto, bordas).


Usando a Biblioteca Rotativa


O Rotativa é o mais fácil de aplicar na minha opinião.


  1. Você já possui um relatório HTML pronto na forma de um modelo e 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. Instale o pacote nuget Rotativa


  2. Adicionar novo controlador para relatório PDF



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

Essencialmente a partir de agora, você retornará um PDF como um arquivo que contém todos os dados do relatório HTML original.


Não descrevi o roteamento aqui, mas entende-se que você configurou rotas para chamar adequadamente os dois controladores


Curiosamente, essa biblioteca em si é essencialmente um invólucro do conhecido utilitário de console wkhtmltopdf . A velocidade do trabalho em altitude, você pode apostar no Azure - funcionará. Mas há recursos sobre os quais falaremos.


Número da Página


É lógico supor que o cliente imprima o PDF e deseje ver o número da página. Tudo é extremamente simples aqui, graças aos criadores do Rotativa.


De acordo com a documentação do Rotativa, através do parâmetro CustomSwitches , CustomSwitches pode especificar os argumentos que serão passados ​​para o wkhtmltopdf utilitário wkhtmltopdf . Bem, as dicas online são generosas com exemplos. A chamada a seguir adiciona um número na parte inferior 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 }; 

Isso funciona muito bem. O próprio número da página é passado usando o parâmetro [page] , esse tipo de parâmetro será substituído por valores específicos.


Além de [page] existem outros:
  • [página] Substituído pelo número de páginas que estão sendo impressas atualmente
  • [frompage] Substituído pelo número da primeira página a ser impressa
  • [topage] Substituído pelo número da última página a ser impressa
  • [página] Substituído pelo URL da página que está sendo impressa
  • [seção] Substituído pelo nome da seção atual
  • [subseção] Substituído pelo nome da subseção atual
  • [data] Substituído pela data atual no formato local do sistema
  • [isodato] Substituído pela data atual no formato estendido ISO 8601
  • [hora] Substituído pela hora atual no formato local do sistema
  • [title] Substituído pelo título do objeto de página atual
  • [doctitle] Substituído pelo título do documento de saída
  • [sitepage] Substituído pelo número da página no site atual sendo convertido
  • [páginas do site] Substituído pelo número de páginas no site atual sendo convertido


Tabela de conteúdo


Relatórios grandes de várias páginas requerem conteúdo em PDF e navegação na página. Isso é muito conveniente e simplesmente vital quando o número de páginas em um relatório excede cem.


O manual wkhtmltopdf contém uma lista completa de todas as opções, entre as quais --toc . Vendo esse parâmetro, o utilitário basicamente coleta todas as tags <h1>, <h2>, ... <h6> no documento e gera um sumário com base nelas. Portanto, é necessário fornecer o uso correto dessas tags de cabeçalho no seu modelo HTML.


Mas, na realidade, adicionar --toc não leva a consequências. Como se não houvesse parâmetro. No entanto, outras opções funcionam. Graças a uma postagem em algum fórum, achei que esse parâmetro deveria ser passado sem hífens: toc . De fato, nesse caso, o conteúdo é adicionado como a primeira página. Ao clicar em uma linha no conteúdo, você acessa a página desejada no documento, os números de página estão corretos.


Ainda não está completamente claro como configurar os estilos, mas ainda não o fiz.


Execução de Javascript


O próximo ponto que encontrei foi a necessidade de adicionar gráficos ao relatório. Minha página HTML contém código JS que adiciona gráficos usando a biblioteca dc.js Aqui está um exemplo:


 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(); } 

Ao mesmo tempo, em HTML, tenho um elemento correspondente:


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

Para que esse código funcione, você precisa importar as bibliotecas apropriadas: dc.js , d3.js , crossfilter.js . Uma chamada para a função initChart criará um gráfico e inserirá o resultado
svg para o item especificado na árvore.


Mas o PDF não contém um traço de gráficos. Assim como qualquer outro rastreamento da execução do código JavaScript antes da renderização do PDF. Verificar isso é muito fácil - basta adicionar o código elementar para criar um elemento <div> simples com texto, apenas para testar o fato da chamada JavaScript.


Empiricamente, verificou-se que a localização do código JS para wkhtmltopdf desempenha um papel significativo. O código localizado no final de <html> ou, digamos, no final de <body> JS não será executado. Parece que o utilitário simplesmente não o nota, ou não espera encontrá-lo lá.


Mas o código dentro do <head> é executado. Assim, cheguei ao esquema quando o código JavaScript está localizado após a declaração de estilos dentro da <head> e é chamado pela construção usual:


 <body onload="initCharts()"> 

Nesse caso, o código será executado conforme o esperado.


Limitações de JavaScript


Mas ainda não havia gráficos no PDF de saída. Então comecei a supor que não sendo um navegador completo, o mecanismo de renderização e execução de pdf provavelmente não é perfeito e não entende as últimas regras. Mais uma vez, experimentalmente, descobri que as funções das setas não são percebidas. Além disso, se o intérprete encontra algo desconhecido para ele, simplesmente pára de funcionar.


Substituindo funções de seta no formato x => x.value por function(x) { return x.value; } mais clássicas function(x) { return x.value; } function(x) { return x.value; } ajudou e todo o código foi executado, o gráfico resultante entrou em um arquivo PDF.


Largura do gráfico


Empiricamente, descobriu-se que era necessário indicar claramente a largura do elemento pai do gráfico. Para isso, especifiquei o estilo do dc-chart . Ele contém a largura do gráfico em pixels. Caso contrário, o gráfico no PDF será muito pequeno, apesar de na versão HTML ocupar toda a largura. A especificação da largura da porcentagem funcionará apenas para HTML.


JavaScript / CSS embutido


Concluindo, gostaria de observar que muitas bibliotecas que convertem HTML em PDF aceitam um determinado baseUrl como parâmetro. Esse é o URL com base no qual o conversor concluirá os caminhos relativos para o recebimento de estilos CSS importados, arquivos ou fontes JavaScirpt. Não posso dizer com certeza como isso funciona no Rotativa, mas criei uma abordagem diferente.


Para acelerar o carregamento inicial do relatório e eliminar a origem dos problemas de incorporação de arquivos de script ou estilo durante a conversão, incorporei as JS e CSS necessárias diretamente no corpo do modelo HTML.


Para fazer isso, crie os pacotes configuráveis ​​apropriados:


 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") ); } } 

Adicione uma chamada de configuração para esses pacotes ao Global.asax.cs


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

E adicione o método apropriado para incorporar o código na página. Ele precisa ser colocado no mesmo espaço de nome que Global.asax.cs para que o método possa ser chamado a partir do modelo 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; } } 

Bem, o toque final é uma chamada do modelo:


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

Como resultado, todo o CSS e JavaScript necessários estarão diretamente em HTML, embora durante o desenvolvimento você possa trabalhar com arquivos individuais.


Muito provavelmente, muitos pensarão imediatamente na ineficiência dessa abordagem em termos de solicitações de cache do navegador. Mas eu tinha dois objetivos específicos:


  • para que o conversor de PDF não precise fazer solicitações de estilos ou códigos em algum lugar e o usuário precise aguardar por isso, respectivamente;
  • para que o primeiro download do relatório em PDF e HTML leve um tempo mínimo, sem a necessidade de aguardar algumas solicitações adicionais. No contexto do meu projeto, isso é importante;

Quebras de página


A estruturação do relatório em seções pode ser acompanhada de requisitos para iniciar uma nova seção a partir de uma nova página. Nesse caso, você pode usar com sucesso a abordagem CSS simples:


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

O utilitário wkhtmltopdf lê com êxito essas classes e entende que é necessário iniciar uma nova página. A primeira classe - page-break-before - informa ao utilitário para sempre iniciar uma nova página com esse elemento. A segunda classe - no-page-break-inside - deve ser aplicada aos elementos que é desejável manter o mais completo possível na página. Por exemplo, você tem blocos consecutivos de informações estruturadas ou diz tabelas. Se dois blocos couberem na página - eles serão localizados. Se o terceiro não couber na página, não será o próximo. Se for maior que uma página, sua transferência já é inevitável. Tudo isso funciona de forma adequada e conveniente.


Comportamento do Flex em wkhtmltopdf


Bem, o último recurso que notei está relacionado ao uso de estilos de marcação flexbox. Todos nos acostumamos a eles e quase toda a marcação é feita por flexões. No entanto, o wkhtmltopdf está um pouco atrasado nesse sentido. As opções flexíveis horizontais não funcionam (pelo menos no meu caso, isso não funcionou. Vi na rede mencionar que vale a pena duplicar os estilos flexíveis da seguinte maneira:


 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; 

Infelizmente, isso não levou à marcação esperada no PDF. Tive que refazer o layout de alguns elementos para que o posicionamento horizontal dos blocos estivesse de acordo com os requisitos. Se alguém tiver experiência bem-sucedida na integração de flexs para wkhtmltopdf, compartilhe. Isso seria muito útil.


Alguns links:


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


All Articles