Articles

Génération de PDF à partir de HTML avec Node.js et Marionnettiste

Image de Máté Boér's Picture

Máté Boér

Développeur Full-Stack chez RisingStack

Dans cet article, je vais montrer comment vous pouvez générer un document PDF à partir d’une page React fortement stylisée à l’aide de Node.js, marionnettiste, Chrome sans tête &Docker.

Contexte: Il y a quelques mois, l’un des clients de RisingStack nous a demandé de développer une fonctionnalité permettant à l’utilisateur de demander une page React au format PDF. Cette page est essentiellement un rapport / résultat pour les patients avec visualisation de données, contenant beaucoup de SVG. De plus, il y avait des demandes spéciales pour manipuler la mise en page et effectuer des réarrangements des éléments HTML. Le PDF devrait donc avoir un style et des ajouts différents par rapport à la page React d’origine.

Comme l’affectation était un peu plus complexe que ce qui aurait pu être résolu avec de simples règles CSS, nous avons d’abord exploré les implémentations possibles. Essentiellement, nous avons trouvé 3 solutions principales. Cet article de blog vous expliquera ces possibilités et les implémentations finales.

Un commentaire personnel avant de commencer: c’est assez compliqué, alors attachez-vous!

Table des matières :

  • Côté client ou côté Backend ?
  • Option 1: Faire une capture d’écran à partir du DOM
  • Option 2: Utiliser uniquement une bibliothèque PDF
  • Option finale 3: Marionnettiste, Chrome sans tête avec Nœud.js
    • Manipulation de style
    • Envoyer le fichier au client et l’enregistrer
  • En utilisant Puppeteer avec Docker
  • Option 3 +1: Règles d’impression CSS
  • Résumé

Côté client ou côté serveur ?

Il est possible de générer un fichier PDF à la fois côté client et côté serveur. Cependant, il est probablement plus logique de laisser le backend le gérer, car vous ne voulez pas utiliser toutes les ressources que le navigateur de l’utilisateur peut offrir.

Même ainsi, je montrerai toujours des solutions pour les deux méthodes.

Option 1: Faire une capture d’écran à partir du DOM

À première vue, cette solution semblait être la plus simple, et elle s’est avérée vraie, mais elle a ses propres limites. Si vous n’avez pas de besoins spéciaux, comme du texte sélectionnable ou consultable dans le PDF, c’est un moyen simple et efficace d’en générer un.

Cette méthode est simple et simple: créez une capture d’écran à partir de la page et placez-la dans un fichier PDF. Assez simple. Nous avons utilisé deux paquets pour cette approche :

Html2canvas, pour faire une capture d’écran à partir du DOM
jsPdf, une bibliothèque pour générer des PDF

Commençons le codage.

npm install html2canvas jspdf

import html2canvas from 'html2canvas'import jsPdf from 'jspdf' function printPDF () { const domElement = document.getElementById('your-id') html2canvas(domElement, { onclone: (document) => { document.getElementById('print-button').style.visibility = 'hidden'}}) .then((canvas) => { const img = canvas.toDataURL('image/png') const pdf = new jsPdf() pdf.addImage(imgData, 'JPEG', 0, 0, width, height) pdf.save('your-filename.pdf')})

Et c’est tout!

Assurez-vous de jeter un œil à la méthode html2canvasonclone. Cela peut s’avérer pratique lorsque vous devez rapidement prendre un instantané et manipuler le DOM (par exemple, masquer le bouton d’impression) avant de prendre la photo. Je peux voir beaucoup de cas d’utilisation pour ce paquet. Malheureusement, le nôtre n’en était pas un, car nous devions gérer la création de PDF du côté backend.

Option 2: Utilisez uniquement une bibliothèque PDF

Il existe plusieurs bibliothèques sur NPM à cet effet, comme jsPDF (mentionné ci-dessus) ou PDFKit. Le problème avec eux est que je devrais recréer la structure de la page si je voulais utiliser ces bibliothèques. Cela nuit définitivement à la maintenabilité, car j’aurais dû appliquer toutes les modifications ultérieures au modèle PDF et à la page React.

Jetez un coup d’œil au code ci-dessous. Vous devez créer le document PDF vous-même à la main. Maintenant, vous pouvez parcourir le DOM et comprendre comment traduire chaque élément en PDF, mais c’est un travail fastidieux. Il doit y avoir un moyen plus facile.

doc = new PDFDocumentdoc.pipe fs.createWriteStream('output.pdf')doc.font('fonts/PalatinoBold.ttf') .fontSize(25) .text('Some text with an embedded font!', 100, 100) doc.image('path/to/image.png', { fit: , align: 'center', valign: 'center'}); doc.addPage() .fontSize(25) .text('Here is some vector graphics...', 100, 100) doc.end()

Cet extrait provient des documents PDFKit. Cependant, cela peut être utile si votre cible est un fichier PDF tout de suite et non la conversion d’une page HTML déjà existante (et en constante évolution).

Option finale 3: Marionnettiste, Chrome sans tête avec Nœud.js

Qu’est-ce que le marionnettiste ? La documentation indique :

Puppeteer est une bibliothèque de nœuds qui fournit une API de haut niveau pour contrôler Chrome ou Chromium sur le protocole DevTools. Puppeteer fonctionne sans tête par défaut, mais peut être configuré pour exécuter Chrome ou Chromium complet (non sans tête).

C’est essentiellement un navigateur que vous pouvez exécuter à partir du nœud.js. Si vous lisez les documents, la première chose qu’il dit à propos de Puppeteer est que vous pouvez l’utiliser pour générer des captures d’écran et des PDF de pages. Excellent! C’est ce que nous cherchions.

Installons Puppeteer avec npmi i puppeteer, et implémentons notre cas d’utilisation.

const puppeteer = require('puppeteer') async function printPDF() { const browser = await puppeteer.launch({ headless: true }); const page = await browser.newPage(); await page.goto('https://blog.risingstack.com', {waitUntil: 'networkidle0'}); const pdf = await page.pdf({ format: 'A4' }); await browser.close(); return pdf})

C’est une fonction simple qui navigue vers une URL et génère un fichier PDF du site.

Tout d’abord, nous lançons le navigateur (génération PDF uniquement prise en charge en mode sans tête), puis nous ouvrons une nouvelle page, définissons la fenêtre d’affichage et naviguons vers l’URL fournie.

La définition de l’option waitUntil: ‘networkidle0’ signifie que Puppeteer considère que la navigation est terminée lorsqu’il n’y a pas de connexion réseau pendant au moins 500 ms. (Consultez les documents de l’API pour plus d’informations.)

Après cela, nous enregistrons le PDF dans une variable, nous fermons le navigateur et renvoyons le PDF.

Remarque: La méthode page.pdf reçoit un objet options, où vous pouvez également enregistrer le fichier sur le disque avec l’option ‘path’. Si le chemin n’est pas fourni, le PDF ne sera pas enregistré sur le disque, vous obtiendrez un tampon à la place. Plus tard, je discute de la façon dont vous pouvez le gérer.)

Si vous devez d’abord vous connecter pour générer un PDF à partir d’une page protégée, vous devez d’abord naviguer vers la page de connexion, inspecter les éléments du formulaire pour l’ID ou le nom, les remplir, puis soumettre le formulaire:

await page.type('#email', process.env.PDF_USER)await page.type('#password', process.env.PDF_PASSWORD)await page.click('#submit')

Stockez toujours les identifiants de connexion dans les variables d’environnement, ne les codez pas en dur!

Manipulation de style

Puppeteer a également une solution pour cette manipulation de style. Vous pouvez insérer des balises de style avant de générer le PDF, et Puppeteer générera un fichier avec les styles modifiés.

await page.addStyleTag({ content: '.nav { display: none} .navbar { border: 0px} #print-button {display: none}' })

Envoyez le fichier au client et enregistrez-le

D’accord, maintenant vous avez généré un fichier PDF sur le backend. Que faire maintenant ?

Comme je l’ai mentionné ci-dessus, si vous n’enregistrez pas le fichier sur le disque, vous obtiendrez un tampon. Il vous suffit d’envoyer ce tampon avec le type de contenu approprié au frontal.

printPDF.then(pdf => {res.set({ 'Content-Type': 'application/pdf', 'Content-Length': pdf.length })res.send(pdf)

Maintenant, vous pouvez simplement envoyer une demande au serveur, pour obtenir le PDF généré.

function getPDF() { return axios.get(`${API_URL}/your-pdf-endpoint`, { responseType: 'arraybuffer', headers: { 'Accept': 'application/pdf' } })

Une fois que vous avez envoyé la demande, le tampon devrait commencer à être téléchargé. Maintenant, la dernière étape consiste à convertir le tampon en fichier PDF.

savePDF = () => { this.openModal(‘Loading…’) // open modal return getPDF() // API call .then((response) => { const blob = new Blob(, {type: 'application/pdf'}) const link = document.createElement('a') link.href = window.URL.createObjectURL(blob) link.download = `your-file-name.pdf` link.click() this.closeModal() // close modal }) .catch(err => /** error handling **/) }
<button onClick={this.savePDF}>Save as PDF</button>

C’était tout! Si vous cliquez sur le bouton enregistrer, le PDF sera enregistré par le navigateur.

Utilisation de Puppeteer avec Docker

Je pense que c’est la partie la plus délicate de l’implémentation – alors laissez-moi vous faire économiser quelques heures de recherche sur Google.

La documentation officielle indique que « la mise en service de Chrome sans tête dans Docker peut être délicate »” Les documents officiels ont une section de dépannage, où au moment de la rédaction, vous pouvez trouver toutes les informations nécessaires sur l’installation de puppeteer avec Docker.

Si vous installez Puppeteer sur l’image Alpine, assurez-vous de faire défiler un peu vers cette partie de la page. Sinon, vous risquez de passer sous silence le fait que vous ne pouvez pas exécuter la dernière version de Puppeteer et vous devez également désactiver l’utilisation de shm, en utilisant un indicateur:

const browser = await puppeteer.launch({ headless: true, args: });

Sinon, le sous-processus Puppeteer pourrait manquer de mémoire avant même qu’il ne démarre correctement. Plus d’informations à ce sujet sur le lien de dépannage ci-dessus.

Option 3 + 1:Règles d’impression CSS

On pourrait penser qu’il est facile d’utiliser simplement des règles d’impression CSS du point de vue des développeurs. Pas de modules NPM, juste du CSS pur. Mais comment s’en sortent-ils en matière de compatibilité entre navigateurs?

Lorsque vous choisissez des règles d’impression CSS, vous devez tester le résultat dans chaque navigateur pour vous assurer qu’il fournit la même mise en page, et ce n’est pas à 100% qu’il le fait.

Par exemple, insérer une pause après un élément donné ne peut pas être considéré comme un cas d’utilisation ésotérique, mais vous pourriez être surpris de devoir utiliser des solutions de contournement pour que cela fonctionne dans Firefox.

À moins que vous ne soyez un magicien CSS endurci avec beaucoup d’expérience dans la création de pages imprimables, cela peut prendre du temps.

Les règles d’impression sont excellentes si vous pouvez garder les feuilles de style d’impression simples.

Voyons un exemple.

@media print { .print-button { display: none; } .content div { break-after: always; }}

Ce CSS ci-dessus masque le bouton d’impression et insère un saut de page après chaque divavec la classe content.Il y a un excellent article qui résume ce que vous pouvez faire avec les règles d’impression, et quelles sont les difficultés avec elles, y compris la compatibilité du navigateur.

En tenant compte de tout, les règles d’impression CSS sont géniales et efficaces si vous souhaitez créer un PDF à partir d’une page pas si complexe.

Résumé: PDF à partir de HTML avec nœud.js et Puppeteer

Passons donc rapidement en revue les options que nous avons abordées ici pour générer des fichiers PDF à partir de pages HTML:

  • Capture d’écran du DOM: Cela peut être utile lorsque vous devez créer des instantanés à partir d’une page (par exemple pour créer une vignette), mais est insuffisant lorsque vous avez beaucoup de données à gérer.
  • Utilisez uniquement une bibliothèque PDF: Si vous devez créer des fichiers PDF par programmation à partir de zéro, c’est une solution parfaite. Sinon, vous devez conserver les modèles HTML et PDF, ce qui est définitivement une interdiction.
  • Marionnettiste: Bien qu’il soit relativement difficile de le faire fonctionner sur Docker, il a fourni le meilleur résultat pour notre cas d’utilisation, et c’était aussi le plus facile à écrire le code.
  • Règles d’impression CSS: Si vos utilisateurs sont suffisamment instruits pour savoir imprimer dans un fichier et que vos pages sont relativement simples, cela peut être la solution la plus indolore. Comme vous l’avez vu dans notre cas, ce n’était pas le cas.

Bonne impression!

Sujets connexes

Nœud.tutoriels js pour débutants / @RisingStack