Generar PDFs
Cómo generar documentos PDF programáticamente desde HTML, plantillas o datos crudos.
Visión General
La generación de PDFs es un requisito común para facturas, reportes, certificados y documentos legales. Las librerías modernas permiten crear PDFs desde plantillas HTML, lo que significa que tu equipo de diseño puede estilizar documentos con CSS mientras tu backend llena datos en vivo. Aqui se explica como los enfoques más confiables en Python, JavaScript y Java.
Cuándo Usar
Usa este recurso cuando:
- Necesites generar facturas, recibos o confirmaciones de orden. Consulta Export CSV Excel para exportación de datos tabulares.
- Los usuarios soliciten reportes o exports de analíticas descargables. Consulta Background Jobs para generación asíncrona de PDFs.
- Debas producir documentos legalmente compliant (contratos, certificados). Consulta Email Templates MJML para entrega profesional por email.
- Quieras reutilizar diseños HTML/CSS existentes para salida impresa. Consulta Image Optimization para optimización de imágenes embebidas.
Solución
Python (WeasyPrint)
from weasyprint import HTML, CSS
from jinja2 import Template
html_template = """
<!DOCTYPE html>
<html>
<head><style>
body { font-family: Arial; margin: 40px; }
h1 { color: #333; }
.total { font-weight: bold; font-size: 1.2em; }
</style></head>
<body>
<h1>Factura #{{ invoice_id }}</h1>
<p>Cliente: {{ customer }}</p>
<p class="total">Total: ${{ total }}</p>
</body>
</html>
"""
def generate_invoice(invoice_id, customer, total):
template = Template(html_template)
html_out = template.render(invoice_id=invoice_id, customer=customer, total=total)
HTML(string=html_out).write_pdf(f"invoice_{invoice_id}.pdf")
generate_invoice("12345", "Acme Corp", "1,250.00")
JavaScript (Puppeteer)
const puppeteer = require("puppeteer");
const handlebars = require("handlebars");
const template = handlebars.compile(`
<html>
<head><style>
body { font-family: Arial; margin: 40px; }
h1 { color: #333; }
.total { font-weight: bold; font-size: 1.2em; }
</style></head>
<body>
<h1>Factura #{{invoiceId}}</h1>
<p>Cliente: {{customer}}</p>
<p class="total">Total: ${{total}}</p>
</body>
</html>
`);
async function generatePDF(data, outputPath) {
const browser = await puppeteer.launch();
const page = await browser.newPage();
const html = template(data);
await page.setContent(html, { waitUntil: "networkidle0" });
await page.pdf({ path: outputPath, format: "A4", printBackground: true });
await browser.close();
}
generatePDF(
{ invoiceId: "12345", customer: "Acme Corp", total: "1,250.00" },
"invoice_12345.pdf"
);
Java (OpenPDF + Thymeleaf)
import com.lowagie.text.Document;
import com.lowagie.text.Paragraph;
import com.lowagie.text.pdf.PdfWriter;
import org.thymeleaf.TemplateEngine;
import org.thymeleaf.context.Context;
import java.io.FileOutputStream;
public class PdfGenerator {
public static void generate(String outputPath, String customer, String total) throws Exception {
TemplateEngine engine = new TemplateEngine();
Context ctx = new Context();
ctx.setVariable("customer", customer);
ctx.setVariable("total", total);
String html = engine.process("invoice-template", ctx);
Document document = new Document();
PdfWriter.getInstance(document, new FileOutputStream(outputPath));
document.open();
document.add(new Paragraph("Factura para " + customer));
document.add(new Paragraph("Total: " + total));
document.close();
}
}
Explicación
Hay dos enfoques principales para generar PDFs:
- HTML-to-PDF: Renderiza HTML+CSS a PDF (WeasyPrint, Puppeteer, wkhtmltopdf). Ideal para layouts complejos y reutilizar diseños web.
- API Nativa: Construye PDFs programáticamente con librerías de bajo nivel (iText, OpenPDF, PDFBox). Ideal para control fino y archivos pequeños.
HTML-to-PDF es el enfoque dominante hoy porque separa presentación (CSS) de datos (variables de plantilla), permitiendo que no-desarrolladores ajusten diseños.
Variantes
| Enfoque | Librería | Pros | Contras |
|---|---|---|---|
| HTML-to-PDF | WeasyPrint | Puro Python, buen CSS | Sin JS, fuentes limitadas |
| HTML-to-PDF | Puppeteer | Motor Chrome completo | Pesado (~100 MB), más lento |
| HTML-to-PDF | Playwright | Moderno, mantenido | Peso similar a Puppeteer |
| API Nativa | iText / OpenPDF | Rápido, archivos pequeños | Código verboso, sin CSS |
| API Nativa | PDFBox | Licencia Apache, maduro | Complejo para docs simples |
Lo que funciona
- Usa plantillas HTML para layouts complejos: Los diseñadores pueden editar CSS sin tocar código.
- Incrusta fuentes: Las fuentes del sistema varían entre SOs. Incrusta una web font para consistencia.
- Configura márgenes y headers/footers: Usa reglas CSS
@pagepara layouts print-friendly. - Genera asíncronamente: La creación de PDFs es intensiva en CPU. Usa una cola para batches grandes.
- Valida input antes de renderizar: Sanitiza HTML para prevenir ataques de inyección en plantillas.
Errores Comunes
- Usar headless Chrome para cada PDF individual: El overhead de inicio es ~1s. Reusa instancias de navegador o usa un pool.
- No incrustar imágenes como base64: URLs de imágenes externas fallan cuando el PDF se ve offline.
- Ignorar saltos de página: Tablas largas se desbordan sin
page-break-inside: avoid. - Hardcodear rutas: Usa directorios temporales o streams, no
/tmp/output.pdf. - Olvidar cerrar el navegador / documento: Fuga memoria y handles de archivo.
Mejores Prácticas Adicionales
- Streamea el output del PDF en vez de escribir a disco. Retorna PDFs como byte streams para evitar I/O de disco y limpieza de archivos temporales. Esto es esencial para deployments serverless donde el espacio en disco es efímero:
from weasyprint import HTML
import io
def generate_pdf_stream(html_content: str) -> bytes:
"""Genera PDF como bytes sin escribir a disco."""
buffer = io.BytesIO()
HTML(string=html_content).write_pdf(buffer)
return buffer.getvalue()
# Flask: return generate_pdf_stream(html), mimetype='application/pdf'
- Usa font subsetting para reducir el tamaño del archivo. Archivos de fuente completos pueden añadir 200KB-2MB por familia. WeasyPrint hace subsetting automáticamente. Para Puppeteer, usa
--font-render-hinting=nonee incrusta solo los pesos que necesitas:
// Incrusta solo los pesos de fuente que realmente usas
const html = `
<style>
@font-face {
font-family: 'Inter';
src: url('data:font/woff2;base64,${interRegularBase64}') format('woff2');
font-weight: 400;
font-style: normal;
}
body { font-family: 'Inter', sans-serif; }
</style>
<h1>Hola Mundo</h1>
`;
- Añade metadata al PDF para buscabilidad. Configura título, autor, asunto y keywords en las propiedades del PDF. Esto mejora la indexación por motores de búsqueda y búsqueda de escritorio:
from weasyprint import HTML
def generate_pdf_with_metadata(html: str, output_path: str, metadata: dict) -> None:
doc = HTML(string=html).render()
doc.pages[0].document.info.update({
'Title': metadata.get('title', ''),
'Author': metadata.get('author', ''),
'Subject': metadata.get('subject', ''),
'Keywords': metadata.get('keywords', ''),
})
doc.write_pdf(output_path)
# generate_pdf_with_metadata(html, "factura.pdf", {
# "title": "Factura #12345",
# "author": "ACME Inc.",
# "subject": "Pago en 30 días",
# "keywords": "factura, acme, 12345"
# })
Errores Comunes Adicionales
- No manejar timeouts en generación de PDFs. HTML complejo con recursos externos puede colgarse indefinidamente. Configura timeouts en Puppeteer y WeasyPrint:
const puppeteer = require('puppeteer');
async function generatePdfWithTimeout(html, timeoutMs = 30000) {
const browser = await puppeteer.launch();
const page = await browser.newPage();
try {
await page.setContent(html, {
waitUntil: 'networkidle0',
timeout: timeoutMs,
});
const pdf = await page.pdf({
format: 'A4',
printBackground: true,
timeout: timeoutMs,
});
return pdf;
} catch (err) {
if (err.name === 'TimeoutError') {
throw new Error(`Generación de PDF timed out después de ${timeoutMs}ms`);
}
throw err;
} finally {
await browser.close();
}
}
- Generar PDFs en el hilo principal en Node.js. La generación de PDFs es intensiva en CPU y bloquea el event loop. Usa worker threads o un proceso separado:
const { Worker } = require('worker_threads');
const path = require('path');
function generatePdfInWorker(html, options) {
return new Promise((resolve, reject) => {
const worker = new Worker(path.join(__dirname, 'pdf-worker.js'), {
workerData: { html, options },
});
worker.on('message', resolve);
worker.on('error', reject);
worker.on('exit', (code) => {
if (code !== 0) reject(new Error(`Worker salió con código ${code}`));
});
});
}
// pdf-worker.js:
// const { parentPort, workerData } = require('worker_threads');
// const puppeteer = require('puppeteer');
// (async () => {
// const browser = await puppeteer.launch();
// const page = await browser.newPage();
// await page.setContent(workerData.html, { waitUntil: 'networkidle0' });
// const pdf = await page.pdf(workerData.options);
// await browser.close();
// parentPort.postMessage(pdf);
// })();
- No testear el output del PDF entre lectores de PDF. Los PDFs se renderizan diferente en Adobe Reader, Chrome, Firefox y Preview. Testea con al menos Chrome y Adobe Reader. Problemas comunes: diferencias de font fallback, soporte de CSS
@page, y renderizado de campos de formulario.
Preguntas frecuentes
Puedo generar un PDF desde un componente React/Vue?
Sí, con Puppeteer o Playwright. Renderiza el componente a HTML en el servidor (SSR), luego pasa el string HTML al motor de PDF. Algunos frameworks (Next.js) ofrecen APIs de exportación a PDF integradas.
Cómo agrego firmas digitales a PDFs?
Usa iText (Java) o PyPDF2 + una librería crypto (Python). Necesitas un certificado X.509 y clave privada. Para producción, usa un hardware security module (HSM) o servicio de firma en la nube (AWS CloudHSM, Azure Key Vault).
Por qué mi PDF es mucho más grande de lo esperado?
Las fuentes incrustadas y las imágenes sin comprimir son los culpables usuales. Usa font subsetting (solo glifos usados) y comprime imágenes antes de incrustar. WeasyPrint y Puppeteer soportan font subsetting.
Recursos Relacionados
Validación de Subida de Archivos
Cómo manejar subidas de archivos de forma segura con validación de tamaño, tipo y contenido.
RecipeEnviar Emails con SMTP en Python, Node.js y Java
Aprende a enviar emails transaccionales y masivos vía SMTP con Python, Node.js y Java. Cubre autenticación, plantillas, adjuntos y entregabilidad.
RecipeLeer y Escribir Archivos
Cómo leer y escribir archivos de forma segura en varios lenguajes de programación.
PatternPatrón Abstract Factory
Crea familias de objetos relacionados sin especificar sus clases concretas. Patrón de diseño creacional para familias de objetos consistentes.
PatternPatrón Adapter
Convierte la interfaz de una clase en otra interfaz que los clientes esperan. Patrón de diseño estructural para compatibilidad de interfaces.
RecipeExportar Datos a CSV y Excel
Cómo exportar datos estructurados a archivos CSV y Excel de forma eficiente.