Volver al inicio
Anexo: Generar PDFs con JavaScript

Generar PDFs con JavaScript

Aprende a crear PDFs dinámicos en el navegador: facturas, reportes, certificados y más, sin necesidad de backend.

1. ¿Por qué generar PDFs con JavaScript?

Ventajas

  • Sin backend: Se genera en el navegador del usuario
  • Descarga inmediata: El usuario descarga el PDF al momento
  • Privacidad: Los datos no se envían a un servidor
  • Económico: Sin coste de procesamiento en servidor

Casos de uso

  • Facturas y recibos
  • Reportes y análisis
  • Certificados y diplomas
  • Presupuestos y cotizaciones

2. Librerías para generar PDFs

jsPDF

Ligera y fácil de usar - RECOMENDADA para principiantes

Instalación:

npm install jspdf
  • Muy ligera (~200KB)
  • Sintaxis simple
  • Soporta texto, imágenes, tablas
  • Menos opciones de estilo que HTML2PDF

html2pdf

Convierte HTML a PDF - MEJOR para layouts complejos

Instalación:

npm install html2pdf.js
  • Usa HTML/CSS existente
  • Más flexible con estilos
  • Genera PDFs complejos
  • Más pesada (~700KB)

PDFKit (Node.js)

Para backend con Node.js

Instalación:

npm install pdfkit
  • Control total del PDF
  • Se ejecuta en servidor
  • No funciona en el navegador

💡 Recomendación: Usaremos jsPDF para este tutorial por su simplicidad y porque genera PDFs directamente en el navegador sin dependencias de HTML.

2.5. Comparativa: html2pdf.js vs PDFMake

Característica html2pdf.js PDFMake
Entrada HTML/CSS existente Definición JSON
Tamaño (minificado) ~700KB ~400KB
Curva de aprendizaje Muy fácil (usas HTML) Media (sintaxis específica)
Control sobre layout Alto (todo CSS) Muy alto (granular)
Tablas ✓ Soporte completo ✓ Muy potente
Imágenes ✓ Directo desde HTML ✓ Base64 o URL
Múltiples páginas ✓ Automático ✓ Manual con control
Estilos avanzados ⚠ Limitado vs CSS ✓ Muy flexible
Watermarks ✗ Complicado ✓ Fácil
Encriptación ✗ No soportado ✓ Soportado
Mejor para... Convertir HTML → PDF fácilmente PDFs complejos y profes.

html2pdf.js - Usa cuando:

  • Ya tienes HTML y CSS diseñados
  • Necesitas convertir rápidamente a PDF
  • El layout es sencillo/estándar
  • Prefieres curva de aprendizaje mínima

PDFMake - Usa cuando:

  • Necesitas control total sobre el PDF
  • Requieres estilos avanzados (watermarks, etc.)
  • Generas PDFs profesionales frecuentemente
  • Necesitas características como encriptación

3. Ejemplo básico: Factura simple

Explicación de argumentos y parámetros

new jsPDF(): Crea un documento PDF vacío. Los parámetros por defecto son A4, milímetros (mm) de unidad.
setFontSize(número): Establece el tamaño de la fuente en puntos (pt). Ejemplo: 20 = título grande, 12 = texto normal.
doc.text('texto', x, y, opciones): Añade texto en las coordenadas (x, y). X=10 es 10mm desde la izquierda, Y=40 es 40mm desde arriba.
doc.line(x1, y1, x2, y2): Dibuja una línea desde (x1,y1) hasta (x2,y2). Útil para separadores.
doc.save('nombre.pdf'): Descarga el PDF con el nombre especificado.
Generar factura simple con jsPDFjavascript
1// Primero instala: npm install jspdf
2
3import { jsPDF } from 'jspdf';
4
5function generarFactura() {
6 // 1. Crear documento (A4, milímetros)
7 const doc = new jsPDF();
8
9 // 2. Títulos y contenido
10 doc.setFontSize(20);
11 doc.text('FACTURA', 105, 20, { align: 'center' });
12
13 doc.setFontSize(12);
14 doc.text('Datos de la empresa:', 10, 40);
15 doc.text('Mi Empresa S.A.', 10, 50);
16 doc.text('CIF: A12345678', 10, 60);
17 doc.text('Teléfono: 666 777 888', 10, 70);
18
19 // 3. Datos del cliente
20 doc.setFontSize(12);
21 doc.text('Cliente:', 10, 90);
22 doc.text('Juan Pérez', 10, 100);
23 doc.text('Email: juan@ejemplo.com', 10, 110);
24
25 // 4. Tabla de items
26 doc.setFontSize(11);
27 doc.text('Concepto', 10, 130);
28 doc.text('Cantidad', 80, 130);
29 doc.text('Precio', 120, 130);
30 doc.text('Total', 160, 130);
31
32 // Línea separadora
33 doc.line(10, 135, 200, 135);
34
35 // Items
36 doc.text('Producto A', 10, 145);
37 doc.text('2', 85, 145);
38 doc.text('50€', 120, 145);
39 doc.text('100€', 160, 145);
40
41 doc.text('Producto B', 10, 155);
42 doc.text('1', 85, 155);
43 doc.text('75€', 120, 155);
44 doc.text('75€', 160, 155);
45
46 // Total
47 doc.setFontSize(14);
48 doc.setFont(undefined, 'bold');
49 doc.text('TOTAL: 175€', 160, 175);
50
51 // 5. Descargar PDF
52 doc.save('factura.pdf');
53}
54
55// Usar en HTML
56// <button onclick="generarFactura()">Descargar Factura PDF</button>

📝 Nota: Este es un ejemplo básico. Los PDFs generados tendrán un aspecto simple pero funcional.

4. Ejemplo mejorado: Factura con datos dinámicos

Explicación de nuevos conceptos

PAGE_WIDTH y PAGE_HEIGHT: Obtienen las dimensiones totales del PDF (297mm x 210mm para A4 apaisado).
Y_POSITION: Variable que controla la posición vertical. Cada vez que escribes algo, debes actualizarla para que no se sobreescriba.
setTextColor(r, g, b): Cambia el color del texto usando RGB (0-255). Ejemplo: (100, 50, 200) = morado.
setFont(undefined, 'bold'): undefined mantiene fuente actual, 'bold' la hace negrita. Puedes usar 'normal' para volver.
align: 'center': En texto, parámetro para centrar. También existe 'left' y 'right'.
forEach(item => ): Itera sobre cada item del array para generar filas dinámicamente.
toFixed(2): Redondea números a 2 decimales. Ejemplo: 36.75 en lugar de 36.7500001.
Factura con datos dinámicos y cálculosjavascript
1import { jsPDF } from 'jspdf';
2
3function generarFacturaDinamica(datos) {
4 const doc = new jsPDF();
5
6 // Estilos
7 const MARGIN = 10;
8 const PAGE_WIDTH = doc.internal.pageSize.getWidth();
9 const PAGE_HEIGHT = doc.internal.pageSize.getHeight();
10 let Y_POSITION = 20;
11
12 // Función auxiliar para espacios
13 const salto = (espacios = 1) => {
14 Y_POSITION += espacios * 7;
15 };
16
17 // Encabezado
18 doc.setFontSize(18);
19 doc.setTextColor(100, 50, 200); // Morado
20 doc.text('FACTURA', PAGE_WIDTH / 2, Y_POSITION, { align: 'center' });
21 doc.setTextColor(0);
22
23 salto(2);
24 doc.setFontSize(10);
25 doc.text(`Número: ${datos.numero}`, PAGE_WIDTH - MARGIN, Y_POSITION, { align: 'right' });
26 doc.text(`Fecha: ${datos.fecha}`, PAGE_WIDTH - MARGIN, Y_POSITION + 7, { align: 'right' });
27
28 salto(3);
29
30 // Datos empresa
31 doc.setFontSize(11);
32 doc.setFont(undefined, 'bold');
33 doc.text('De:', MARGIN, Y_POSITION);
34
35 doc.setFont(undefined, 'normal');
36 doc.text(datos.empresa.nombre, MARGIN, Y_POSITION + 7);
37 doc.setFontSize(9);
38 doc.text(datos.empresa.direccion, MARGIN, Y_POSITION + 14);
39 doc.text(`CIF: ${datos.empresa.cif}`, MARGIN, Y_POSITION + 21);
40
41 // Datos cliente
42 doc.setFontSize(11);
43 doc.setFont(undefined, 'bold');
44 doc.text('Para:', PAGE_WIDTH / 2, Y_POSITION);
45
46 doc.setFont(undefined, 'normal');
47 doc.text(datos.cliente.nombre, PAGE_WIDTH / 2, Y_POSITION + 7);
48 doc.setFontSize(9);
49 doc.text(datos.cliente.email, PAGE_WIDTH / 2, Y_POSITION + 14);
50
51 salto(6);
52
53 // Tabla de items
54 doc.setFontSize(10);
55 doc.setFont(undefined, 'bold');
56
57 // Encabezados
58 doc.text('Descripción', MARGIN, Y_POSITION);
59 doc.text('Cant.', MARGIN + 100, Y_POSITION);
60 doc.text('Precio', MARGIN + 125, Y_POSITION);
61 doc.text('Total', MARGIN + 155, Y_POSITION);
62
63 // Línea
64 doc.line(MARGIN, Y_POSITION + 3, PAGE_WIDTH - MARGIN, Y_POSITION + 3);
65
66 salto(2);
67 doc.setFont(undefined, 'normal');
68
69 // Items dinámicos
70 let subtotal = 0;
71 datos.items.forEach(item => {
72 const total = item.cantidad * item.precio;
73 subtotal += total;
74
75 doc.text(item.descripcion, MARGIN, Y_POSITION);
76 doc.text(String(item.cantidad), MARGIN + 105, Y_POSITION);
77 doc.text(`${item.precio}€`, MARGIN + 130, Y_POSITION);
78 doc.text(`${total}€`, MARGIN + 160, Y_POSITION);
79
80 salto(2);
81 });
82
83 // Línea final
84 doc.line(MARGIN, Y_POSITION - 3, PAGE_WIDTH - MARGIN, Y_POSITION - 3);
85
86 salto(2);
87
88 // Cálculos
89 doc.setFont(undefined, 'bold');
90 doc.text('Subtotal:', MARGIN + 120, Y_POSITION);
91 doc.text(`${subtotal}€`, MARGIN + 160, Y_POSITION);
92
93 salto();
94 const iva = subtotal * 0.21;
95 doc.text('IVA (21%):', MARGIN + 120, Y_POSITION);
96 doc.text(`${iva.toFixed(2)}€`, MARGIN + 160, Y_POSITION);
97
98 salto();
99 const total = subtotal + iva;
100 doc.setTextColor(100, 50, 200);
101 doc.setFontSize(12);
102 doc.text('TOTAL:', MARGIN + 120, Y_POSITION);
103 doc.text(`${total.toFixed(2)}€`, MARGIN + 160, Y_POSITION);
104
105 // Descargar
106 doc.save(`factura-${datos.numero}.pdf`);
107}
108
109// Usar:
110const datosFactura = {
111 numero: 'FAC-2024-001',
112 fecha: '2024-11-26',
113 empresa: {
114 nombre: 'Mi Empresa S.A.',
115 direccion: 'Calle Principal 123, 28001 Madrid',
116 cif: 'A12345678'
117 },
118 cliente: {
119 nombre: 'Juan Pérez García',
120 email: 'juan@ejemplo.com'
121 },
122 items: [
123 { descripcion: 'Producto A', cantidad: 2, precio: 50 },
124 { descripcion: 'Producto B', cantidad: 1, precio: 75 },
125 { descripcion: 'Servicio C', cantidad: 5, precio: 20 }
126 ]
127};
128
129generarFacturaDinamica(datosFactura);

5. Usando html2pdf (para layouts complejos)

Explicación de opciones de html2pdf

element: El elemento HTML que quieres convertir a PDF. Se obtiene con document.getElementById().
margin: Espacios en mm alrededor del contenido. Ejemplo: 10 = 10mm de margen en todos lados.
filename: Nombre del archivo que se descargará.
image.type: Formato de imagen usado (jpeg, png). 'jpeg' es más ligero.
image.quality: Calidad de imagen (0-1). 0.98 = muy alta, 0.7 = media.
html2canvas.scale: Escala de renderizado (2 = doble resolución, más claro pero más pesado).
jsPDF.format: Tamaño de página ('a4', 'a3', etc). 'portrait' = vertical, 'landscape' = apaisado.
Generar PDF desde HTML existentejavascript
1// npm install html2pdf.js
2
3import html2pdf from 'html2pdf.js';
4
5function generarFacturaHTML() {
6 // 1. Crear el HTML de la factura
7 const element = document.getElementById('factura-template');
8
9 // 2. Opciones de configuración
10 const opt = {
11 margin: 10,
12 filename: 'factura.pdf',
13 image: { type: 'jpeg', quality: 0.98 },
14 html2canvas: { scale: 2 },
15 jsPDF: { orientation: 'portrait', unit: 'mm', format: 'a4' }
16 };
17
18 // 3. Generar y descargar
19 html2pdf().set(opt).from(element).save();
20}
21
22// HTML a usar:
23```html
24<div id="factura-template" style="padding: 20px; font-family: Arial;">
25 <h1 style="text-align: center; color: #6432c8;">FACTURA</h1>
26
27 <div style="display: grid; grid-template-columns: 1fr 1fr; gap: 20px; margin-top: 30px;">
28 <div>
29 <h3>De:</h3>
30 <p><strong>Mi Empresa S.A.</strong></p>
31 <p>Calle Principal 123</p>
32 <p>CIF: A12345678</p>
33 </div>
34
35 <div>
36 <h3>Para:</h3>
37 <p><strong>Cliente XYZ</strong></p>
38 <p>Email: cliente@ejemplo.com</p>
39 <p style="color: #666;">Número: FAC-2024-001</p>
40 </div>
41 </div>
42
43 <table style="width: 100%; margin-top: 30px; border-collapse: collapse;">
44 <thead>
45 <tr style="background: #f0f0f0; font-weight: bold;">
46 <th style="padding: 10px; text-align: left;">Concepto</th>
47 <th style="padding: 10px; text-align: center;">Cantidad</th>
48 <th style="padding: 10px; text-align: right;">Precio</th>
49 <th style="padding: 10px; text-align: right;">Total</th>
50 </tr>
51 </thead>
52 <tbody>
53 <tr style="border-bottom: 1px solid #ddd;">
54 <td style="padding: 10px;">Producto A</td>
55 <td style="padding: 10px; text-align: center;">2</td>
56 <td style="padding: 10px; text-align: right;">50€</td>
57 <td style="padding: 10px; text-align: right;">100€</td>
58 </tr>
59 <tr style="border-bottom: 1px solid #ddd;">
60 <td style="padding: 10px;">Producto B</td>
61 <td style="padding: 10px; text-align: center;">1</td>
62 <td style="padding: 10px; text-align: right;">75€</td>
63 <td style="padding: 10px; text-align: right;">75€</td>
64 </tr>
65 </tbody>
66 </table>
67
68 <div style="text-align: right; margin-top: 20px; font-weight: bold;">
69 <p>Subtotal: 175€</p>
70 <p>IVA (21%): 36,75€</p>
71 <p style="color: #6432c8; font-size: 18px; margin-top: 10px;">TOTAL: 211,75€</p>
72 </div>
73</div>
74```
75
76// Botón en HTML:
77// <button onclick="generarFacturaHTML()">Descargar PDF</button>

✓ Ventaja: Con html2pdf puedes reutilizar HTML y CSS que ya tengas en tu proyecto. Perfecta para layouts complejos con tablas, imágenes, etc.

5.5. Usando PDFMake (Control total y profesional)

Explicación de estructura PDFMake

docDefinition: Objeto JSON que define toda la estructura del PDF.
pageSize: Tamaño de página ('A4', 'A3', 'letter', etc).
pageMargins: Array [izquierda, arriba, derecha, abajo] en puntos (72 puntos = 1 pulgada).
header y footer: Contenido que se repite en cada página.
content: Array con todo lo que va en el PDF (texto, tablas, columnas, etc).
columns: Divide contenido en columnas (útil para lado a lado, como empresa y cliente).
table: Define tablas con headerRows (filas de encabezado), widths (ancho de columnas), body (datos).
fillColor: Color de fondo (hex: '#6432c8' o RGB: [100, 50, 200]).
alignment: 'center', 'left', 'right' para alineación del texto.
margin: Espaciado alrededor de elementos (similar a CSS).
Factura profesional con PDFMakejavascript
1// npm install pdfmake
2
3import pdfMake from 'pdfmake/build/pdfmake';
4import pdfFonts from 'pdfmake/build/vfs_fonts';
5
6pdfMake.vfs = pdfFonts.pdfMake.vfs;
7
8function generarFacturaPDFMake() {
9 const docDefinition = {
10 pageSize: 'A4',
11 pageMargins: [40, 40, 40, 40],
12
13 header: {
14 text: 'FACTURA',
15 alignment: 'center',
16 fontSize: 20,
17 bold: true,
18 color: '#6432c8',
19 margin: [0, 0, 0, 20]
20 },
21
22 content: [
23 // Número y fecha
24 {
25 columns: [
26 {
27 text: [
28 { text: 'Número: ', bold: true },
29 'FAC-2024-001'
30 ],
31 width: '50%'
32 },
33 {
34 text: [
35 { text: 'Fecha: ', bold: true },
36 new Date().toLocaleDateString('es-ES')
37 ],
38 width: '50%',
39 alignment: 'right'
40 }
41 ],
42 margin: [0, 0, 0, 20]
43 },
44
45 // Empresa y Cliente
46 {
47 columns: [
48 {
49 width: '50%',
50 stack: [
51 { text: 'De:', bold: true, fontSize: 12 },
52 'Mi Empresa S.A.',
53 'Calle Principal 123',
54 'CIF: A12345678'
55 ]
56 },
57 {
58 width: '50%',
59 stack: [
60 { text: 'Para:', bold: true, fontSize: 12 },
61 'Cliente XYZ',
62 'Email: cliente@ejemplo.com',
63 'Teléfono: 666 777 888'
64 ]
65 }
66 ],
67 margin: [0, 0, 0, 30]
68 },
69
70 // Tabla de items
71 {
72 table: {
73 headerRows: 1,
74 widths: ['40%', '15%', '15%', '15%'],
75 body: [
76 // Headers
77 [
78 { text: 'Concepto', bold: true, color: 'white', fillColor: '#6432c8' },
79 { text: 'Cantidad', bold: true, color: 'white', fillColor: '#6432c8', alignment: 'center' },
80 { text: 'Precio', bold: true, color: 'white', fillColor: '#6432c8', alignment: 'right' },
81 { text: 'Total', bold: true, color: 'white', fillColor: '#6432c8', alignment: 'right' }
82 ],
83 // Items
84 ['Producto A', { text: '2', alignment: 'center' }, { text: '50€', alignment: 'right' }, { text: '100€', alignment: 'right' }],
85 ['Producto B', { text: '1', alignment: 'center' }, { text: '75€', alignment: 'right' }, { text: '75€', alignment: 'right' }],
86 ['Servicio C', { text: '5', alignment: 'center' }, { text: '20€', alignment: 'right' }, { text: '100€', alignment: 'right' }]
87 ]
88 },
89 margin: [0, 0, 0, 30]
90 },
91
92 // Totales
93 {
94 alignment: 'right',
95 columns: [
96 {
97 width: '40%',
98 stack: [
99 { text: 'Subtotal:', margin: [0, 5, 0, 5] },
100 { text: 'IVA (21%):', margin: [0, 5, 0, 5] },
101 { text: 'TOTAL:', bold: true, fontSize: 14, color: '#6432c8', margin: [0, 10, 0, 5] }
102 ]
103 },
104 {
105 width: '20%',
106 stack: [
107 { text: '275€', margin: [0, 5, 0, 5] },
108 { text: '57,75€', margin: [0, 5, 0, 5] },
109 { text: '332,75€', bold: true, fontSize: 14, color: '#6432c8', margin: [0, 10, 0, 5] }
110 ]
111 }
112 ]
113 }
114 ],
115
116 footer: {
117 text: 'Gracias por su compra',
118 alignment: 'center',
119 fontSize: 10,
120 color: '#999',
121 margin: [0, 20, 0, 0]
122 }
123 };
124
125 // Generar y descargar
126 pdfMake.createPdf(docDefinition).download('factura-2024-001.pdf');
127}
128
129// Usar:
130// <button onclick="generarFacturaPDFMake()">Descargar PDF con PDFMake</button>

🎯 Ventajas de este enfoque:

  • ✓ Estilos aplicados directamente en el JSON
  • ✓ Control pixel-perfect del layout
  • ✓ Más ligero que html2pdf
  • ✓ Fácil de generar PDFs desde datos dinámicos
  • ✓ Soporte para watermarks, encriptación, etc.

6. Caso práctico: Generar factura en Astro

Factura completamente funcional en Astroastro
1---
2// factura.astro
3
4// Datos de ejemplo (podrían venir de una API)
5const factura = {
6 numero: 'FAC-2024-001',
7 fecha: new Date().toLocaleDateString('es-ES'),
8 empresa: {
9 nombre: 'Mi Tienda Online',
10 direccion: 'Calle Principal 123, Madrid',
11 cif: 'A12345678'
12 },
13 cliente: {
14 nombre: 'Juan Pérez García',
15 email: 'juan@ejemplo.com'
16 },
17 items: [
18 { descripcion: 'Laptop', cantidad: 1, precio: 999 },
19 { descripcion: 'Mouse', cantidad: 2, precio: 25 },
20 { descripcion: 'Teclado', cantidad: 1, precio: 79 }
21 ]
22};
23
24const subtotal = factura.items.reduce((sum, item) => sum + (item.cantidad * item.precio), 0);
25const iva = subtotal * 0.21;
26const total = subtotal + iva;
27---
28
29<html>
30<head>
31 <title>Factura {factura.numero}</title>
32</head>
33<body>
34 <div id="factura-contenido">
35 <h1>FACTURA</h1>
36 <p><strong>Número:</strong> {factura.numero}</p>
37 <p><strong>Fecha:</strong> {factura.fecha}</p>
38
39 <h2>Datos de Empresa</h2>
40 <p>{factura.empresa.nombre}</p>
41 <p>{factura.empresa.direccion}</p>
42
43 <h2>Cliente</h2>
44 <p>{factura.cliente.nombre}</p>
45 <p>{factura.cliente.email}</p>
46
47 <h2>Items</h2>
48 <table border="1" cellpadding="10">
49 <tr>
50 <th>Concepto</th>
51 <th>Cantidad</th>
52 <th>Precio</th>
53 <th>Total</th>
54 </tr>
55 {factura.items.map(item => (
56 <tr>
57 <td>{item.descripcion}</td>
58 <td>{item.cantidad}</td>
59 <td>{item.precio}€</td>
60 <td>{item.cantidad * item.precio}€</td>
61 </tr>
62 ))}
63 </table>
64
65 <h2>Resumen</h2>
66 <p><strong>Subtotal:</strong> {subtotal}€</p>
67 <p><strong>IVA (21%):</strong> {iva.toFixed(2)}€</p>
68 <p style="font-size: 18px; color: purple;">
69 <strong>TOTAL: {total.toFixed(2)}€</strong>
70 </p>
71 </div>
72
73 <button id="descargar-btn"> Descargar PDF</button>
74
75 <script>
76 import html2pdf from 'html2pdf.js';
77
78 document.getElementById('descargar-btn').addEventListener('click', () => {
79 const element = document.getElementById('factura-contenido');
80 const opt = {
81 margin: 10,
82 filename: 'factura-001.pdf',
83 image: { type: 'jpeg', quality: 0.98 },
84 html2canvas: { scale: 2 },
85 jsPDF: { orientation: 'portrait', unit: 'mm', format: 'a4' }
86 };
87
88 html2pdf().set(opt).from(element).save();
89 });
90 </script>
91
92 <style>
93 body { font-family: Arial; padding: 20px; }
94 table { width: 100%; margin: 20px 0; }
95 button {
96 padding: 10px 20px;
97 background: purple;
98 color: white;
99 border: none;
100 border-radius: 5px;
101 cursor: pointer;
102 }
103 </style>
104</body>
105</html>

8. Consejos y mejores prácticas

📏 Tamaño de página

Usa A4 (210x297mm) por defecto. Es el estándar para facturas y documentos.

🎨 Márgenes

Establece márgenes de 10-15mm en todos los lados para que se vea profesional.

🖼️ Imágenes y logos

Convierte imágenes a base64 o usa URLs públicas. Asegúrate de que se vean bien en el PDF.

📱 Responsividad

El PDF siempre se genera con el mismo tamaño, pero asegúrate de que el contenido quepa en una página A4.

⚙️ Rendimiento

Para PDFs muy complejos, considera generar en el backend. En el navegador puede ser lento con documentos grandes.

✅ Testing

Prueba en diferentes navegadores. Las renderizaciones pueden variar entre Chrome, Firefox y Safari.

Resumen

jsPDF es ideal para PDFs simples generados programáticamente
html2pdf.js para layouts complejos reutilizando HTML existente
PDFMake para control total y PDFs profesionales con JSON
Genera PDFs sin backend - más rápido, seguro y privado
Perfecto para facturas, reportes, certificados y cotizaciones
Compatible con Astro y cualquier framework JavaScript moderno

Para ver como quedaria en la práctica

Aqui veras como queda con cada una de las librerias explicadas.

Ir a Demo Interactiva