Generar PDFs desde HTML con Python: WeasyPrint y pdfkit
Generar PDFs a partir de HTML es la forma más flexible de crear documentos con diseño complejo: facturas, reportes, certificados, tickets. Python ofrece tres opciones principales: WeasyPrint (Python puro, CSS moderno), pdfkit (wrapper de wkhtmltopdf) y xhtml2pdf (más simple, sin dependencias externas).
Comparativa de herramientas
| Herramienta | CSS moderno | Sin dependencias externas | JavaScript | Velocidad |
|---|---|---|---|---|
| WeasyPrint | Sí (CSS3) | Sí | No | Media |
| pdfkit (wkhtmltopdf) | Sí (WebKit) | No (requiere binario) | Sí | Rápida |
| xhtml2pdf | Limitado | Sí | No | Rápida |
| Playwright→PDF | Chrome completo | No (Chromium) | Sí | Lenta |
WeasyPrint
Instalación
pip install weasyprint
# En Linux, instalar dependencias del sistema:
apt-get install libpango-1.0-0 libharfbuzz0b libpangoft2-1.0-0
HTML básico a PDF
import weasyprint
def html_a_pdf(html_contenido, ruta_pdf, base_url=None):
"""
Convierte HTML a PDF.
base_url: URL base para resolver rutas relativas (CSS, imágenes)
"""
weasyprint.HTML(string=html_contenido, base_url=base_url).write_pdf(ruta_pdf)
print(f"PDF generado: {ruta_pdf}")
html = """
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<style>
body { font-family: Arial, sans-serif; margin: 2cm; }
h1 { color: #2E5984; border-bottom: 2px solid #2E5984; }
table { width: 100%; border-collapse: collapse; }
th { background: #2E5984; color: white; padding: 8px; }
td { padding: 6px; border-bottom: 1px solid #ddd; }
</style>
</head>
<body>
<h1>Reporte de Ventas</h1>
<p>Generado el: 24 de abril de 2026</p>
<table>
<tr><th>Producto</th><th>Cantidad</th><th>Total</th></tr>
<tr><td>Laptop Pro</td><td>3</td><td>€3.899,97</td></tr>
<tr><td>Monitor 4K</td><td>5</td><td>€2.745,00</td></tr>
</table>
</body>
</html>
"""
html_a_pdf(html, "reporte.pdf")
Desde archivo HTML + CSS externo
import weasyprint
from pathlib import Path
def archivo_html_a_pdf(ruta_html, ruta_pdf, ruta_css=None):
html = weasyprint.HTML(filename=str(ruta_html))
stylesheets = []
if ruta_css:
stylesheets.append(weasyprint.CSS(filename=str(ruta_css)))
html.write_pdf(str(ruta_pdf), stylesheets=stylesheets)
print(f"PDF: {ruta_pdf}")
archivo_html_a_pdf("factura.html", "factura.pdf", "estilos_pdf.css")
Cabecera y pie de página con CSS @page
html_con_cabecera = """
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<style>
@page {
size: A4;
margin: 2.5cm 2cm 3cm 2cm;
@top-center {
content: "Mi Empresa S.L. — Confidencial";
font-size: 9pt;
color: #666;
}
@bottom-left {
content: "Generado: 24/04/2026";
font-size: 8pt;
color: #888;
}
@bottom-center {
content: "Página " counter(page) " de " counter(pages);
font-size: 9pt;
}
@bottom-right {
content: "www.miempresa.com";
font-size: 8pt;
color: #888;
}
}
body {
font-family: "Helvetica Neue", Arial, sans-serif;
font-size: 11pt;
line-height: 1.5;
color: #333;
}
h1 { page-break-before: avoid; }
table { page-break-inside: avoid; }
.nueva-pagina { page-break-before: always; }
</style>
</head>
<body>
<h1>Informe Anual 2026</h1>
<p>Contenido del informe...</p>
<div class="nueva-pagina">
<h1>Apéndice</h1>
<p>Datos adicionales...</p>
</div>
</body>
</html>
"""
import weasyprint
weasyprint.HTML(string=html_con_cabecera).write_pdf("informe_con_cabecera.pdf")
Factura profesional con WeasyPrint + Jinja2
from jinja2 import Template
import weasyprint
from datetime import date
TEMPLATE_FACTURA = """
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<style>
@page { size: A4; margin: 1.5cm; }
body { font-family: Arial, sans-serif; font-size: 10pt; color: #333; }
.cabecera { display: flex; justify-content: space-between; margin-bottom: 2em; }
.logo { font-size: 24pt; font-weight: bold; color: #2E5984; }
.num-fac { font-size: 18pt; font-weight: bold; color: #666; }
.datos-empresa { text-align: right; color: #555; }
.cliente { background: #f5f5f5; padding: 1em; margin: 1em 0; border-radius: 4px; }
table { width: 100%; border-collapse: collapse; margin: 1.5em 0; }
th { background: #2E5984; color: white; padding: 8px; text-align: left; }
td { padding: 6px 8px; border-bottom: 1px solid #e0e0e0; }
.total-row { font-weight: bold; background: #f5f5f5; }
.iva-row { color: #555; }
.importe { text-align: right; }
</style>
</head>
<body>
<div class="cabecera">
<div>
<div class="logo">{{ empresa.nombre }}</div>
<div class="datos-empresa">
{{ empresa.direccion }}<br>
NIF: {{ empresa.nif }}<br>
{{ empresa.email }} | {{ empresa.web }}
</div>
</div>
<div class="num-fac">
FACTURA #{{ numero }}<br>
<small>Fecha: {{ fecha }}</small>
</div>
</div>
<div class="cliente">
<strong>Facturar a:</strong><br>
{{ cliente.nombre }}<br>
NIF: {{ cliente.nif }}<br>
{{ cliente.direccion }}
</div>
<table>
<tr>
<th>Descripción</th>
<th>Cantidad</th>
<th class="importe">Precio unit.</th>
<th class="importe">Total</th>
</tr>
{% for linea in lineas %}
<tr>
<td>{{ linea.descripcion }}</td>
<td>{{ linea.cantidad }}</td>
<td class="importe">{{ "%.2f"|format(linea.precio) }} €</td>
<td class="importe">{{ "%.2f"|format(linea.cantidad * linea.precio) }} €</td>
</tr>
{% endfor %}
<tr class="iva-row">
<td colspan="3">Base imponible</td>
<td class="importe">{{ "%.2f"|format(subtotal) }} €</td>
</tr>
<tr class="iva-row">
<td colspan="3">IVA (21%)</td>
<td class="importe">{{ "%.2f"|format(iva) }} €</td>
</tr>
<tr class="total-row">
<td colspan="3">TOTAL</td>
<td class="importe">{{ "%.2f"|format(total) }} €</td>
</tr>
</table>
<p style="font-size: 9pt; color: #888;">
Forma de pago: Transferencia bancaria — IBAN: ES76 2100 0418 4502 0005 1332<br>
Vencimiento: 30 días desde la fecha de factura
</p>
</body>
</html>
"""
def generar_factura(datos, ruta_pdf):
lineas = datos['lineas']
subtotal = sum(l['cantidad'] * l['precio'] for l in lineas)
iva = subtotal * 0.21
total = subtotal + iva
template = Template(TEMPLATE_FACTURA)
html = template.render(
**datos,
subtotal=subtotal,
iva=iva,
total=total,
fecha=date.today().strftime("%d/%m/%Y"),
)
weasyprint.HTML(string=html).write_pdf(ruta_pdf)
print(f"Factura generada: {ruta_pdf}")
generar_factura({
"numero": "2026-042",
"empresa": {
"nombre": "TechSoluciones S.L.",
"nif": "B12345678",
"direccion": "Calle Mayor 1, 28001 Madrid",
"email": "info@techsoluciones.com",
"web": "www.techsoluciones.com",
},
"cliente": {
"nombre": "Empresa Cliente S.A.",
"nif": "A87654321",
"direccion": "Avda. Diagonal 200, 08013 Barcelona",
},
"lineas": [
{"descripcion": "Desarrollo web (40h)", "cantidad": 40, "precio": 75.00},
{"descripcion": "Alojamiento anual", "cantidad": 1, "precio": 240.00},
{"descripcion": "Mantenimiento mensual", "cantidad": 3, "precio": 150.00},
],
}, "factura_2026-042.pdf")
pdfkit (wkhtmltopdf)
pip install pdfkit
# Instalar wkhtmltopdf:
# Ubuntu: apt-get install wkhtmltopdf
# Windows: descargar de https://wkhtmltopdf.org/downloads.html
import pdfkit
opciones = {
'page-size': 'A4',
'margin-top': '2cm',
'margin-right': '1.5cm',
'margin-bottom': '2.5cm',
'margin-left': '1.5cm',
'encoding': 'UTF-8',
'footer-center': 'Página [page] de [topage]',
'footer-font-size': '8',
'enable-local-file-access': None,
}
# Desde cadena HTML
pdfkit.from_string("<h1>Hola mundo</h1>", "salida.pdf", options=opciones)
# Desde URL
pdfkit.from_url("https://example.com", "pagina.pdf", options=opciones)
# Desde archivo
pdfkit.from_file("reporte.html", "reporte.pdf", options=opciones)
xhtml2pdf (sin dependencias externas)
pip install xhtml2pdf
from xhtml2pdf import pisa
import io
def html_a_pdf_xhtml2pdf(html_contenido, ruta_pdf):
with open(ruta_pdf, "wb") as f:
resultado = pisa.CreatePDF(
io.StringIO(html_contenido),
dest=f,
encoding='utf-8'
)
if resultado.err:
print(f"Error: {resultado.err}")
else:
print(f"PDF generado: {ruta_pdf}")
html_a_pdf_xhtml2pdf("""
<html><body>
<h1 style="color: blue">Título</h1>
<p>Texto del documento.</p>
</body></html>
""", "simple.pdf")
Recurso adicional
Para convertir documentos HTML o Word a PDF sin necesidad de programar, usa KaijuConverter — gratis, rápido y sin registro.
Conversiones relacionadas
Conversiones de documento que siguen este tema: