De nombreux développeurs sont confrontés à la tâche de créer des rapports PDF pour les applications Web, une demande tout à fait naturelle. Je voudrais attirer votre attention sur mon expérience d'une telle tâche lors de l'utilisation de la bibliothèque Rotativa pour générer des rapports. C'est à mon avis l'une des bibliothèques les plus pratiques à cet effet dans son segment, mais en l'utilisant, je suis tombé sur plusieurs points non évidents dont je veux parler.
Pour être tout à fait honnête, je voudrais partager l'ensemble des râteaux sur lesquels j'ai marché dans le processus d'intégration de cette bibliothèque, sans aucun doute rapide et très pratique.
Dans cet article, je n'aborderai pas la question du choix d'une bibliothèque. Chacun peut avoir ses propres raisons d'utiliser ceci ou cela. J'ai choisi Rotativa, car avec des coûts d'installation minimes, il a tout ce qu'il faut pour couvrir les besoins des clients. En plus d'elle, j'ai essayé trois ou quatre options supplémentaires.
Énoncé du problème
Application Web sur ASP.NET MVC, .NET version 4.6. D'autres fonctionnalités ne sont pas pertinentes dans ce contexte, à l'exception du déploiement. Le déploiement devrait avoir lieu sur Azure. Ceci est important car certaines autres bibliothèques (par exemple HiQPdf) ne transfèrent pas les installations dans certaines configurations Azure, cela est documenté.
J'ai besoin d'ouvrir un rapport HTML statique avec un lien et une version PDF du même rapport avec le deuxième lien. Le rapport lui-même est simplement un ensemble de quelques tableaux, champs et graphiques pour démonstration à l'utilisateur. Les deux versions nécessitent un menu avec navigation dans les sections du rapport, la présence de tableaux, quelques graphiques (couleurs, taille du texte, bordures).
Utilisation de la bibliothèque Rotativa
Rotativa est aussi facile à appliquer que possible à mon avis.
- Vous disposez déjà d'un rapport HTML prêt à l'emploi sous la forme d'un modèle et d'un contrôleur ASP.NET MVC, tel que celui-ci:
[HttpGet] public async Task<ActionResult> Index(int param1, string param2) { var model = await service.GetReportDataAsync(param1, param2); return View(model); }
Installer le paquet nuget Rotativa
Ajouter un nouveau contrôleur pour le rapport PDF
[HttpGet] public async Task<ActionResult> Pdf(int param1, string param2) { var model = await service.GetReportDataAsync(param1, param2); return new ViewAsPdf("Index", model); }
Essentiellement à partir de maintenant, vous avez un PDF renvoyé sous forme de fichier contenant toutes les données du rapport HTML d'origine.
Je n'ai pas décrit le routage ici, mais il est entendu que vous avez configuré des routes pour appeler correctement les deux contrôleurs
Fait intéressant, cette bibliothèque elle-même est essentiellement un wrapper sur l'utilitaire de console bien connu wkhtmltopdf . La vitesse de travail en altitude, vous pouvez parier sur Azure - fonctionnera. Mais il y a des fonctionnalités dont nous allons parler.
Numéro de page
Il est logique de supposer que le client imprimera le PDF et souhaite voir le numéro de page. Ici, tout est extrêmement simple, grâce aux créateurs de Rotativa.
Selon la documentation Rotativa, via le paramètre CustomSwitches
, CustomSwitches
pouvez spécifier les arguments qui seront transmis à l'utilitaire wkhtmltopdf
. Eh bien, les conseils en ligne sont généreux avec des exemples. L'appel suivant ajoute un numéro au bas de chaque page:
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 };
Cela fonctionne très bien. Le numéro de page lui-même est transmis à l'aide du paramètre [page]
, ce type de paramètres sera remplacé par des valeurs spécifiques.
En plus de [page]
il y en a d'autres:
- [page] Remplacé par le nombre de pages en cours d'impression
- [frompage] Remplacé par le numéro de la première page à imprimer
- [topage] Remplacé par le numéro de la dernière page à imprimer
- [page Web] Remplacé par l'URL de la page en cours d'impression
- [section] Remplacé par le nom de la section actuelle
- [sous-section] Remplacé par le nom de la sous-section actuelle
- [date] Remplacé par la date actuelle au format local du système
- [isodate] Remplacé par la date actuelle au format étendu ISO 8601
- [heure] Remplacé par l'heure actuelle au format local du système
- [title] Remplacé par le titre de l'objet de la page courante
- [doctitle] Remplacé par le titre du document de sortie
- [sitepage] Remplacé par le numéro de la page du site en cours de conversion
- [sitepages] Remplacé par le nombre de pages du site en cours de conversion
Table des matières
Les grands rapports multipages nécessitent un contenu PDF et une navigation dans les pages. C'est très pratique et tout simplement vital lorsque le nombre de pages d'un rapport dépasse la centaine.
Le manuel wkhtmltopdf contient une liste complète de toutes les options, parmi lesquelles --toc
. En voyant ce paramètre, l'utilitaire collecte essentiellement toutes les balises <h1>, <h2>, ... <h6>
dans le document et génère une table des matières en fonction de celles-ci. En conséquence, il est nécessaire de prévoir l'utilisation correcte de ces balises d'en-tête dans votre modèle HTML.
Mais en réalité, l'ajout de --toc
n'entraîne aucune conséquence. Comme s'il n'y avait pas de paramètre. Cependant, d'autres options fonctionnent. Grâce à un post sur un forum, j'ai trouvé que ce paramètre devait être passé sans tirets: toc
. En effet, dans ce cas, le contenu est ajouté en toute première page. Lorsque vous cliquez sur une ligne dans le contenu, vous accédez à la page souhaitée dans le document, les numéros de page sont corrects.
La façon de configurer les styles n'est pas encore complètement claire, mais je ne l'ai pas encore fait.
Exécution de Javascript
Le point suivant que j'ai rencontré était la nécessité d'ajouter des graphiques au rapport. Ma page HTML contient du code JS qui ajoute des graphiques à l'aide de la bibliothèque dc.js
Voici un exemple:
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(); }
En même temps, en HTML, j'ai un élément correspondant:
<div id="chart_C2" class="dc-chart"></div>
Pour que ce code fonctionne, vous devez importer les bibliothèques appropriées: dc.js
, d3.js
, crossfilter.js
. Un appel à la fonction initChart
créera un graphique et insérera le résultat
svg à l'élément spécifié dans l'arborescence.
Mais le PDF ne contient pas de trace de graphiques. Ainsi que toute autre trace d'exécution de code JavaScript avant le rendu PDF. La vérification est assez simple - il suffit d'ajouter le code élémentaire pour créer un élément <div>
simple avec du texte, juste pour tester le fait de l'appel JavaScript.
Empiriquement, il s'est avéré que l'emplacement du code JS pour wkhtmltopdf
joue un rôle important. Le code situé à la fin de <html>
ou, disons, à la fin de <body>
JS ne sera pas exécuté. Il semble que l'utilitaire ne le remarque tout simplement pas, ou ne s'attend pas à le rencontrer là-bas.
Mais le code à l'intérieur de la <head>
est exécuté. Ainsi, je suis arrivé au schéma lorsque le code JavaScript se trouve après la déclaration de styles à l'intérieur de la <head>
, et est appelé par la construction habituelle:
<body onload="initCharts()">
Dans ce cas, le code sera exécuté comme prévu.
Limitations de JavaScript
Mais il n'y avait toujours pas de graphiques dans le PDF de sortie de toute façon. Puis j'ai commencé à deviner que n'étant pas un navigateur complet, le moteur de rendu et d'exécution pdf n'est probablement pas parfait et ne comprend pas les dernières règles. Encore une fois, expérimentalement, j'ai découvert que les fonctions fléchées ne sont pas perçues. De plus, si l'interprète trouve quelque chose d'inconnu pour lui, alors il cesse tout simplement de fonctionner.
Remplacement des fonctions fléchées de la forme x => x.value
par des function(x) { return x.value; }
plus classiques function(x) { return x.value; }
function(x) { return x.value; }
aidé et tout le code a été exécuté, le graphique résultant est entré dans un fichier PDF.
Largeur du graphique
Empiriquement, il s'est avéré qu'il était nécessaire d'indiquer clairement la largeur de l'élément parent du graphique. Pour cela, j'ai spécifié le style de dc-chart
. Il contient la largeur du graphique en pixels. Sinon, le graphique sur le PDF sera très petit, malgré le fait que dans la version HTML, il occupera toute la largeur. La spécification de la largeur en pourcentage ne fonctionnera que pour HTML.
JavaScript / CSS en ligne
En conclusion, je voudrais noter que de nombreuses bibliothèques convertissant HTML en PDF acceptent une certaine baseUrl comme paramètre. Il s'agit de l'URL sur la base de laquelle le convertisseur complétera les chemins relatifs pour recevoir les styles CSS, les fichiers JavaScirpt ou les polices importés. Je ne peux pas dire avec certitude comment cela fonctionne dans Rotativa, mais j'ai trouvé une approche différente.
Pour accélérer le chargement initial du rapport et éliminer la source des problèmes d'incorporation de fichiers de script ou de style lors de la conversion, j'incorpore les JS et CSS nécessaires directement dans le corps du modèle HTML.
Pour ce faire, créez les bundles appropriés:
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") ); } }
Ajoutez un appel de configuration pour ces bundles à Global.asax.cs
protected void Application_Start() { ... BundleConfig.RegisterBundles(BundleTable.Bundles); }
Et ajoutez la méthode appropriée pour incorporer le code dans la page. Il doit être placé dans le même espace de noms que Global.asax.cs
pour que la méthode puisse être appelée à partir du modèle 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; } }
Eh bien, la touche finale est un appel du modèle:
@Html.InlineStyles("~/Styles/report-pdf"); @Html.InlineScripts("~/Scripts/charts");
Par conséquent, tous les CSS et JavaScript nécessaires seront directement en HTML, bien que pendant le développement, vous puissiez travailler avec des fichiers individuels.
Très probablement, beaucoup penseront immédiatement à l'inefficacité de cette approche en termes de demandes de mise en cache par le navigateur. Mais j'avais deux objectifs spécifiques:
- de sorte que le convertisseur PDF ne doive pas faire de demande quelque part pour les styles ou le code, et l'utilisateur doit attendre cela, respectivement;
- afin que le premier téléchargement de rapport PDF et HTML prenne un minimum de temps, sans avoir à attendre quelques demandes supplémentaires. Dans le cadre de mon projet, c'est important;
Sauts de page
La structuration du rapport en sections peut être accompagnée d'exigences pour commencer une nouvelle section à partir d'une nouvelle page. Dans ce cas, vous pouvez utiliser avec succès l'approche CSS simple:
.page-break-before { page-break-before: always; } .no-page-break-inside { page-break-before: auto; page-break-inside: avoid; }
L'utilitaire wkhtmltopdf lit avec succès ces classes et comprend qu'il est nécessaire de démarrer une nouvelle page. La première classe - page-break-before
- indique à l'utilitaire de toujours commencer une nouvelle page avec cet élément. La deuxième classe - no-page-break-inside
- doit être appliquée aux éléments qu'il est souhaitable de conserver aussi complets que possible sur la page. Par exemple, vous avez des blocs consécutifs d'informations structurées, ou dites des tables. Si deux blocs tiennent sur la page - ils seront localisés. Si le troisième ne rentre pas dans la page, ce ne sera pas le suivant. S'il est plus grand qu'une page, son transfert est déjà inévitable. Tout cela fonctionne de manière adéquate et pratique.
Comportement Flex dans wkhtmltopdf
Eh bien, la dernière fonctionnalité que j'ai remarquée est liée à l'utilisation des styles de balisage flexbox. Nous nous sommes tous habitués à eux et presque tout le balisage est fait par des flexions. Cependant, wkhtmltopdf est un peu en retard à cet égard. Les options de flexion horizontale ne fonctionnent pas (du moins dans mon cas, cela n'a pas fonctionné. J'ai vu dans le réseau mentionner qu'il valait la peine de dupliquer les styles de flex comme suit:
display: -webkit-flex; display: flex; flex-direction: row; -webkit-flex-direction: row; -webkit-box-pack: justify; -webkit-justify-content: space-between; justify-content: space-between;
Mais malheureusement, cela n'a pas conduit au balisage attendu dans le PDF. J'ai dû refaire la disposition de certains éléments pour que le placement horizontal des blocs soit conforme aux exigences. Si quelqu'un a une expérience réussie d'intégration de flexes pour wkhtmltopdf, veuillez partager. Ce serait très utile.
Quelques liens: