Weasyprint est une bibliothèque Python (ainsi qu'un utilitaire en ligne de commande utilisant cette bibliothèque) pour convertir un document HTML5 en PDF en utilisant la CSS d'impression.
Son utilisation est relativement simple, par exemple
from weasyprint import HTML, CSS html = my_template() data = HTML(string=html).write_pdf()
Si nécessaire, il peut être utile de :
modifier le code HTML avant de le donner à Weasyprint, par exemple en utilisant BeautifulSoup pour supprimer ou modifier des éléments de la page, par exemple pour retirer certaines CSS
from bs4 import BeautifulSoup soup = BeautifulSoup(html, 'html.parser') for css in soup.findAll("link"): for cssname in ('typography.com', 'default.css', 'logged-in.css'): if cssname in css['href']: css.extract()
fournir une CSS spécifique à Weasyprint en ajoutant par exemple
stylesheets=[CSS(url=base_url+'/pdf_print.css')])
ajouter des pieds de page ou en-tête via CSS
@page { margin: 3cm 2cm; @bottom-right { content: "Page " counter(page) } @top-center { content: "Pilot Systems"; } }
Weasyprint est très utile pour générer des versions PDF de pages existant déjà en HTML, mais aussi pour réaliser des vues spécifiques d'impression. Le principal avantage dans ce cas est que, s'appuyant sur du HTML et du CSS, il s'intègre parfaitement dans les processus de développement (ainsi que des langages de templating, ...) d'applications Web.
Cependant le rendu possible est parfois limité par ce qui peut être exprimé en CSS, et les CSS d'impression sont parfois délicates à débuguer.
Reportlab est une bibliothèque Python de génération PDF.
L'écosystème Reportlab est un peu complexe en raison des licences, il y a 3 outils différents en réalité :
Pour les utilisateurs de Zope, il existe de plus un produit Zope nommé RMLPageTemplate intégrant le RML et les page template Zope.
Par exemple pour positionner un cadre jaune fluo d'avertissement sur un document provisoire on peut réaliser quelque chose du type
<tal:if tal:condition="not: formulas/validated"> <fill color="yellow"/> <stroke color="black"/> <rect x="115mm" y="217mm" width="90mm" height="18mm" fill="yes" stroke="yes"/> <frame id="warning" x1="115mm" y1="213mm" width="90mm" height="24mm" /> <fill color="black"/> <stroke color="black"/> </tal:if> <story> <para style="warning" tal:condition="not: formulas/validated"> CECI EST UN DOCUMENT PROVISOIRE NON VALIDÉ - NE PAS IMPRIMER </para> </story>
Reportlab fourni beaucoup de fonctionnalités, mais ses points forts sont la génération de graphes et la possibilité d'un placement fin des éléments.
Chez Pilot Systems, nous nous en servons principalement lorsqu'il s'agit de compléter un fond PDF de formulaire qui nous est fourni, en ajoutant les éléments saisi par l'utilisateur (et/ou provenant de la base de données et de calculs) dans les cases du formulaire.
pdftk est une boite à outils permettant de manipuler des PDF. Elle ne permet pas directement de générer des PDFs, mais permet de les transformer ensuite, par exemple extraire des pages, concaténer des PDF, ou imprimer un calque en surimpression sur un fond.
Il s'utilise en ligne de commande, en Python il faut donc utiliser subprocess (ou os.system ou équivalent).
LaTeX est un système de composition de documents très largement utilisé dans le monde de la publication scientifique, mais pas seulement.
Son utilisation en Python est relativement simple : générer un document LaTeX soit directement, soit en passant par un moteur de template, soit même en passant par un langage intermédiaire plus simple type reStructuredText, puis composer le document en appelant l'exécutable pdflatex.
LaTeX est extrêmement puissant et flexible, et donne un rendu d'une qualité inégalée. Il peut générer automatiquement des tables des matières, des index, des glossaires, etc. C'est donc l'outil parfait pour générer des rapports riches.
Son utilisation est cependant plus complexe et peut rebuter le néophyte.
Quelques autres outils et pistes possibles sont listées rapidement :
Lorsqu'on renvoie au navigateur un document PDF, il faut indiquer le type de document via l'en-tête HTTP Content-Type, avec quelque chose du type
response.setHeader('Content-Type', 'application/pdf')
Il est aussi possible d'indiquer au navigateur si on souhaite que le document soit affiché dans la fenêtre du navigateur (pour les navigateurs qui le supportent) ou téléchargé, via l'en-tête Content-Disposition, par exemple
response.setHeader('Content-Disposition', 'attachment; filename="%s.pdf"' % filename)
Attention, il s'agit d'un souhait, le comportement exact peut varier d'un navigateur à l'autre (et suivant la configuration de l'utilisateur).
Lorsqu'on génère des PDF de taille important (plusieurs dizaines ou centaines de pages), quelle que soit la technologie utilisée, le temps de génération peut être long, plusieurs minutes facilement, ce qui peut provoquer des timeout à plusieurs niveaux.
Les solutions envisageables sont :
Augmenter les temps de timeout sur tous les composants impliqués (apache, gunicorn, varnish, etc.). Penser à prévenir l'utilisateur, et à prévoir un mécanisme (par exemple désactiver le bouton en JS une fois que l'utilisateur a cliqué dessus) pour éviter que l'utilisateur impatient ne provoque plusieurs générations du même document en parallèle.
Informer l'utilisateur que son document va lui être transmis par mail, lancer la génération dans un processus annexe (via fork ou via un ordonnanceur de tâches comme Celery ou équivalent), et envoyer un mail avec soit le document en pièce jointe (mais attention les gros mails peuvent être bloqués), soit un lien de téléchargement dans le mail. Penser dans ce cas à nettoyer les fichiers laissés sur le serveur au bout de quelques jours (ou semaines), via un cron par exemple
find /path/to/generated/pdfs -mtime +14 -delete
Lancer le processus en asynchrone, et mettre en place un mécanisme en JavaScript pour interroger le serveur régulièrement, et indiquer à l'utilisateur quand le fichier est généré.
Souvent la génération PDF est réservée à des utilisateurs authentifiés sur le site, mais parfois elle est disponible pour les anonymes. Par exemple une fonctionnalité régulièrement demandée est de mettre un bouton "PDF" sur toutes les pages d'un CMS pour permettre aux visiteurs de les récupérer.
Dans ce cas il faut faire très attention à ce que les moteurs de recherche, ou autres robots, ne se mettent pas à demander une version PDF de toutes les pages, ce qui peut rendre le site temporairement indisponible.
Il y a plusieurs solutions palliatives pour ce problème :
Ce point est lié aux deux précédents. La génération de PDF peut demander des quantités importantes de mémoire (en particulier Weasyprint peut prendre plus d'1 Go lorsqu'on génère des fichiers de plusieurs centaines de pages), et de multiples générations en parallèle peuvent utiliser massivement le CPU.
Quelques astuces pour limiter ces problèmes :
Avant toute mise en production, effectuer une estimation des ressources nécessaires, et dimensionner en conséquence l'hébergement.
Comme pour le temps de génération élevé, utiliser un ordonnanceur de tâches afin de s'assurer qu'une seule génération est effectuée à la fois.
Si la génération est faite via un processus externe (Weasyprint, pdftk, LaTeX, ...) il peut être recommandé d'utiliser taskset (pour affecter un cœur de CPU dédié au processus) et/ou nice (pour affecter une priorité au programme), par exemple en utilisant un petit script de ce type pour envelopper les appels
#!/bin/sh exec nice -n 10 taskset -c 0 /usr/bin/pdftk "$@"
Mettre en place un système de cache, pour garder les PDF générés et ne les générer de nouveau que si les données ont changé.
Ce problème concerne surtout Weasyprint, mais peut aussi concerner d'autres solutions similaires : parfois certaines ressources référencées par un document (images, CSS, ...) ne sont pas accessibles publiquement mais nécessitent d'être authentifiés.
Trois solutions existent alors :
Un scénario qui revient dans plusieurs projets est le suivant :
C'est un cas typique d'utilisation de Weasyprint + pdftk (ou de LaTeX). Mais parfois, à la génération, le tout échoue sans que la raison ne soit évidente.
En fait l'utilisateur a saisi un PDF protégé via du DRM, et l'outil pdftk (ou pdflatex) n'arrive pas à l'intégrer dans son propre PDF (ce serait violer les conditions du document).
Il existe des techniques de contournement, mais qui peuvent poser des problèmes de légalité.
Une autre solution consiste à rejeter les PDF protégés par du DRM lors de la validation du formulaire, en utilisant un outil comme pdfinfo (venant de poppler ) par exemple
st, out = commands.getstatusoutput('pdfinfo %s' % pdffile) if st or re.search('Encrypted:.*yes', out): raise ValueError, "this PDF file is DRM protected"
Actions sur le document