đ Django & PDF â GĂ©nĂ©ration, Manipulation & Tools
Guide opérationnel : WeasyPrint, ReportLab, Templates Django et manipulation de fichiers.
Concept : HTML vs Canvas
Deux écoles : Dessiner pixels (ReportLab) ou convertir du HTML/CSS (WeasyPrint).
Théorie Choix TechnoWeasyPrint : Le Standard
Le moteur HTML/CSS vers PDF moderne et puissant. Installation et fonctionnement.
WeasyPrint HTML2PDFDépendances SystÚme
Installation des libs C requises (Pango, Cairo, GDK-Pixbuf) sur Linux/Mac.
apt install Libraries CReportLab : Le "Bas Niveau"
Génération programmatique haute performance. Difficile mais trÚs rapide.
ReportLab CanvasDjango + WeasyPrint
Transformer un Template Django en PDF. render_to_string.
CSS pour l'Impression
RÚgles @page, numéros de page, sauts de page (break-inside).
Servir le PDF (FileResponse)
Afficher dans le navigateur ou forcer le téléchargement.
FileResponse Content-TypeComparatif Librairies
WeasyPrint vs ReportLab vs xhtml2pdf vs wkhtmltopdf.
Benchmark FeaturesManipulation (pypdf)
Fusionner (Merge), Diviser (Split) et tourner des PDFs existants.
pypdf MergerGénération Asynchrone (Celery)
Ne jamais gĂ©nĂ©rer de gros PDF dans la requĂȘte HTTP. Utiliser des Workers.
Celery PerformanceGestion Fichiers Statiques
ProblĂšmes d'images et CSS (base_url, staticfiles) dans les PDFs.
Static ImagesCheat-sheet PDF
Snippets rapides pour Views, CSS et pypdf.
cheat Snippets1. Approche "Template" (WeasyPrint, xhtml2pdf)
Principe : Vous réutilisez vos connaissances Web. Vous écrivez du HTML/CSS (comme une page web), et un moteur de rendu le convertit en PDF.
- Avantages : Rapide à développer, facile à maintenir, réutilisation des templates Django.
- Inconvénients : Plus lent en CPU/RAM, mise en page "pixel perfect" parfois complexe.
- Leader :
WeasyPrint.
2. Approche "Programmation" (ReportLab)
Principe : Vous écrivez du code Python pour dire "Dessine un rectangle en X=100, Y=200". C'est comme dessiner sur un Canvas.
- Avantages : ExtrĂȘmement rapide (gĂ©nĂ©ration de masse), contrĂŽle total au millimĂštre prĂšs, pas de moteur de rendu lourd.
- Inconvénients : TrÚs verbeux, difficile à maintenir, nécessite d'apprendre une API complexe.
- Leader :
ReportLab.
Pourquoi WeasyPrint ?
C'est la solution recommandée pour 90% des projets Django. Elle supporte le CSS moderne (Flexbox, Grid partiel, @page) et produit des PDFs propres.
Installation Python
# Dans votre venv pip install WeasyPrint
Exemple Minimal (Python)
from weasyprint import HTML
# Depuis une chaĂźne
HTML(string='<h1>Hello</h1>').write_pdf("hello.pdf")
# Depuis une URL
HTML('https://google.com').write_pdf("google.pdf")WeasyPrint nécessite des librairies systÚme (GTK, Pango) pour fonctionner (voir carte 1.3).
Ubuntu / Debian
Ces librairies sont nécessaires pour le rendu des polices et des graphiques.
sudo apt-get install build-essential python3-dev python3-pip \
python3-setuptools python3-wheel python3-cffi libcairo2 \
libpango-1.0-0 libpangoft2-1.0-0 libpangocairo-1.0-0 \
libgdk-pixbuf2.0-0 libffi-dev shared-mime-infoFedora / RHEL / CentOS
sudo dnf install redhat-rpm-config python3-devel \
gcc libffi-devel python3-cffi cairo pango gdk-pixbuf2MacOS (avec Homebrew)
brew install pango libffi cairo gdk-pixbuf
Dockerfile (Base Python Slim)
FROM python:3.11-slim
# Install WeasyPrint system dependencies
RUN apt-get update && apt-get install -y \
build-essential \
libpango-1.0-0 \
libpangoft2-1.0-0 \
libpangocairo-1.0-0 \
libgdk-pixbuf2.0-0 \
libffi-dev \
libcairo2 \
&& rm -rf /var/lib/apt/lists/*
RUN pip install WeasyPrint djangoInstallation
pip install reportlab
Concept : Canvas
L'objet `Canvas` est une feuille blanche. On utilise des coordonnées (X, Y) partant du coin bas-gauche (0,0).
from reportlab.pdfgen import canvas
def create_pdf(filename):
c = canvas.Canvas(filename)
# Dessiner un texte (x=100, y=750)
c.drawString(100, 750, "Hello World from ReportLab")
# Sauvegarder
c.save()Platypus (Haut Niveau)
ReportLab offre une couche "haute" appelée Platypus qui gÚre les paragraphes et les tableaux automatiquement (flowables).
from reportlab.platypus import SimpleDocTemplate, Paragraph
from reportlab.lib.styles import getSampleStyleSheet
doc = SimpleDocTemplate("doc.pdf")
styles = getSampleStyleSheet()
story = []
story.append(Paragraph("Ceci est un paragraphe long qui va se wrapper...", styles['Normal']))
doc.build(story)views.py
On utilise render_to_string pour générer le HTML, puis on le passe à WeasyPrint.
from django.http import HttpResponse
from django.template.loader import render_to_string
from weasyprint import HTML, CSS
def download_invoice(request, invoice_id):
# 1. Récupérer les données
invoice = Invoice.objects.get(pk=invoice_id)
# 2. Rendre le template HTML en string
html_string = render_to_string('pdf/invoice.html', {
'invoice': invoice
})
# 3. Générer le PDF
# (base_url est important pour les images statiques)
html = HTML(string=html_string, base_url=request.build_absolute_uri())
pdf_file = html.write_pdf()
# 4. Retourner la réponse HTTP
response = HttpResponse(pdf_file, content_type='application/pdf')
response['Content-Disposition'] = f'filename="facture_{invoice_id}.pdf"'
return responsetemplates/pdf/invoice.html
<!DOCTYPE html>
<html>
<head>
<style>
@page {
size: A4;
margin: 2cm;
@bottom-right {
content: "Page " counter(page) " / " counter(pages);
}
}
body { font-family: sans-serif; }
.header { color: #234A26; border-bottom: 2px solid #333; }
table { width: 100%; border-collapse: collapse; }
th, td { border: 1px solid #ccc; padding: 8px; }
</style>
</head>
<body>
<div class="header">
<h1>Facture #</h1>
</div>
<p>Client : </p>
<table>
<thead><tr><th>Item</th><th>Prix</th></tr></thead>
<tbody>
</tbody>
</table>
</body>
</html>La rĂšgle @page
Définit les marges et la taille du papier.
@page {
size: A4; /* ou Letter, A3 */
margin: 2.5cm;
margin-top: 3cm; /* Espace pour le header */
/* Zones de page (Headers/Footers) */
@top-center {
content: "Mon Entreprise SAS";
font-weight: bold;
}
@bottom-right {
/* Compteurs automatiques */
content: "Page " counter(page) " sur " counter(pages);
font-size: 10px;
}
}ContrĂŽle des sauts de page
Crucial pour éviter qu'un tableau ou une image soit coupé en deux.
/* EmpĂȘcher de couper un Ă©lĂ©ment */
.no-break {
page-break-inside: avoid;
break-inside: avoid;
}
/* Forcer un saut de page avant */
.new-page {
page-break-before: always;
break-before: always;
}
/* Tables : répéter le header sur chaque page */
thead {
display: table-header-group;
}
tr {
page-break-inside: avoid;
}Utilisez FileResponse (plus adapté que HttpResponse pour les fichiers binaires).
Option 1 : Afficher dans le navigateur (Preview)
from django.http import FileResponse import io # buffer = io.BytesIO(pdf_content) return FileResponse(buffer, as_attachment=False, filename='rapport.pdf')
Option 2 : Forcer le téléchargement
return FileResponse(buffer, as_attachment=True, filename='rapport.pdf')
Note sur les Buffers
Si vous générez le PDF en RAM (sans le sauvegarder sur disque), utilisez io.BytesIO.
import io buffer = io.BytesIO() html.write_pdf(target=buffer) # Ecrit dans la RAM buffer.seek(0) # Revenir au début du fichier return FileResponse(buffer, ...)
| Librairie | Méthode | CSS Support | Performance | Utilisation recommandée |
|---|---|---|---|---|
| WeasyPrint | HTML/CSS | Excellent (CSS3, Flexbox) | Moyen | Factures, Rapports designés, Documents standards. |
| ReportLab | Code Python | N/A (Canvas) | TrÚs Haute | Génération de masse (milliers de docs), Relevés bancaires. |
| xhtml2pdf | HTML/CSS | Faible (CSS 2.1) | Moyen | Vieux projets. Déprécié au profit de WeasyPrint. |
| wkhtmltopdf | Moteur Webkit | Bon | Bon | Nécessite binaire externe. Projet abandonné/archivé. |
Conclusion
Utilisez WeasyPrint par dĂ©faut. Si vous avez des contraintes de performance extrĂȘmes (1000 PDFs / seconde), passez Ă ReportLab.
Pour fusionner, découper ou tourner des PDFs existants. (Anciennement PyPDF2, maintenant pypdf).
pip install pypdf
Fusionner (Merge)
from pypdf import PdfWriter
merger = PdfWriter()
# Ajouter des fichiers
merger.append("intro.pdf")
merger.append("chapitre1.pdf")
merger.append("fin.pdf")
# Sauvegarder le résultat
merger.write("document_complet.pdf")
merger.close()Lire et Extraire
from pypdf import PdfReader, PdfWriter
reader = PdfReader("source.pdf")
writer = PdfWriter()
# Extraire la page 1 seulement
page = reader.pages[0]
writer.add_page(page)
# Sauvegarder
with open("page1.pdf", "wb") as fp:
writer.write(fp)Le ProblĂšme
Générer un PDF prend du temps (0.5s à 10s). Si vous le faites dans la vue Django, vous bloquez le serveur (Timeout Gateway 504).
La Solution : Celery
On délÚgue la tùche à un worker.
# tasks.py
from celery import shared_task
from weasyprint import HTML
from django.core.files.base import ContentFile
from .models import Rapport
@shared_task
def generate_rapport_pdf_task(rapport_id):
rapport = Rapport.objects.get(id=rapport_id)
html_string = render_to_string('rapport.html', {'r': rapport})
# Génération binaire
pdf_bytes = HTML(string=html_string).write_pdf()
# Sauvegarde dans un champ FileField
rapport.pdf_file.save(f'rapport_{rapport_id}.pdf', ContentFile(pdf_bytes))
rapport.save()L'utilisateur reçoit une rĂ©ponse immĂ©diate "GĂ©nĂ©ration en cours...", puis tĂ©lĂ©charge le fichier une fois prĂȘt (polling ou email).
ProblĂšme : Images Introuvables
WeasyPrint ne devine pas oĂč sont vos fichiers CSS/Images locaux si vous utilisez des URLs relatives (ex: /static/logo.png) sans contexte.
Solution 1 : base_url
# Dans la vue HTML(string=html, base_url=request.build_absolute_uri())
Solution 2 : django-weasyprint
Un wrapper pratique qui gĂšre les staticfiles finder automatiquement.
Solution 3 : Chemins absolus (Fichier systĂšme)
Dans le template, pointer directement vers le disque (utile en dev/prod mixte).
WeasyPrint Django View
import io
from django.http import FileResponse
from django.template.loader import render_to_string
from weasyprint import HTML
def pdf_view(request):
# 1. Template -> HTML
html_txt = render_to_string("doc.html", {})
# 2. HTML -> PDF (Buffer)
buf = io.BytesIO()
HTML(string=html_txt,
base_url=request.build_absolute_uri()
).write_pdf(buf)
buf.seek(0)
# 3. Response
return FileResponse(buf, filename="doc.pdf")CSS Print Snippets
/* Format A4 */
@page { size: A4; margin: 2cm; }
/* Saut de page forcé */
.break { page-break-before: always; }
/* Ne pas couper au milieu */
.nobreak { page-break-inside: avoid; }
/* Numérotation */
@page {
@bottom-center {
content: counter(page);
}
}Pypdf Merge (One-linerish)
from pypdf import PdfWriter
merger = PdfWriter()
[merger.append(f) for f in ["a.pdf", "b.pdf"]]
merger.write("result.pdf")
merger.close()
