| name | html-to-pdf-report-pipeline |
| version | 1.0.0 |
| description | Pipeline completo para generar informes PDF profesionales desde HTML/JS vanilla: generación en browser, transferencia a disco, conversión con weasyprint, manejo de charts/maps como imágenes estáticas. Patrón para dashboards que necesitan exportar a PDF sin Puppeteer/Chromium.
|
| tags | ["pdf","html","weasyprint","reports","pipeline","browser","charts","maps"] |
HTML→PDF Report Pipeline — Informes profesionales sin Puppeteer
Resumen
Pipeline para generar informes PDF de 60+ páginas desde aplicaciones web vanilla JS.
Cubre: generación HTML en browser → rasterización de charts/maps → transferencia a disco → conversión PDF con weasyprint.
| Fase | Herramienta | Output |
|---|
| Generación HTML | generarInformeCompleto() en browser | HTML 160KB+, 22 capítulos |
| Rasterización charts | canvas.toDataURL() | PNG embebido en HTML |
| Rasterización maps | html2canvas o exportMapAsImage() | PNG embebido en HTML |
| Transferencia | HTTP receiver o base64 chunks | HTML en disco |
| Conversión PDF | weasyprint input.html output.pdf | PDF A4 profesional |
Arquitectura del pipeline
Browser (generarInformeCompleto)
│
├── Canvas charts → toDataURL() → <img> embebido
├── Leaflet maps → html2canvas → <img> embebido
└── HTML enriquecido (164KB+)
│
├── [Opción A] HTTP receiver POST → disco
├── [Opción B] Base64 chunks → disco
└── [Opción C] Node.js directo (si no usa DOM)
│
▼
weasyprint input.html output.pdf
│
▼
PDF A4 profesional (60+ págs)
Decision Guide
¿El informe usa Canvas/Leaflet/JS?
├── NO → Generar con Node.js directo (sin browser)
│ └── weasyprint directo
│
├── SÍ → ¿Tienes Puppeteer/Chromium?
│ ├── SÍ → Puppeteer (mejor fidelidad)
│ └── NO → weasyprint (limitaciones conocidas)
│ ├── Rasterizar charts como PNG primero
│ ├── Rasterizar maps como PNG primero
│ └── weasyprint con @page CSS
│
└── ¿Necesitas transferir HTML del browser al disco?
├── <50KB → base64 chunks (1-2 rounds)
├── 50-200KB → HTTP receiver (recomendado)
└── >200KB → Node.js directo (evitar transferencia)
Fase 1: Generación HTML en browser
const { generarInformeCompleto } = await import('./js/report.js');
const html = generarInformeCompleto(appState);
window.__fullReport = html;
console.log(`HTML generado: ${Math.round(html.length/1024)}KB`);
Pitfall: generarInformeCompleto() puede fallar silenciosamente si appState tiene datos incompletos. Verificar antes:
const app = window.pmstApp.appState;
console.assert(app.diagnostico, 'Falta diagnostico');
console.assert(app.medidas?.length > 0, 'Falta medidas');
Fase 2: Rasterización de charts (Canvas→PNG)
document.querySelectorAll('canvas').forEach(c => {
const img = document.createElement('img');
img.src = c.toDataURL('image/png');
img.style.width = c.style.width || '100%';
img.style.height = c.style.height || 'auto';
c.parentNode.replaceChild(img, c);
});
Pitfall: Los charts de Chart.js usan devicePixelRatio para nitidez. toDataURL() preserva la resolución original.
Fase 3: Rasterización de maps (Leaflet→PNG)
const mapEl = document.getElementById('map');
const canvas = await html2canvas(mapEl);
const img = document.createElement('img');
img.src = canvas.toDataURL();
mapEl.parentNode.replaceChild(img, mapEl);
await exportMapAsImage('map-container', 'mapa.png');
Pitfall: Leaflet necesita que el mapa esté renderizado y visible para capturar. Si el mapa está en un tab oculto, renderizarlo primero.
Fase 4: Transferencia Browser→Disco
Ver references/browser-to-pdf-patterns.md en skill vanilla-js-dashboard-patterns para los 3 patrones detallados.
Resumen rápido:
- <50KB: base64 chunks
- 50-200KB: HTTP receiver (recomendado)
-
200KB: Node.js directo
Fase 5: Conversión con weasyprint
weasyprint input.html output.pdf
CSS esencial para A4
@page {
size: A4;
margin: 25mm 20mm 30mm 20mm;
@bottom-center {
content: "PLAN DE MOVILIDAD — Hecho con ❤️ por David Antizar";
font-size: 8pt;
color: #666;
font-family: 'Segoe UI', Arial, sans-serif;
}
@bottom-right {
content: counter(page);
font-size: 8pt;
color: #666;
}
}
@media print {
body { margin: 0; padding: 0; }
.no-print { display: none !important; }
.page-break { page-break-before: always; }
h1, h2, h3, h4 { page-break-after: avoid; }
table, figure { page-break-inside: avoid; }
p { orphans: 3; widows: 3; }
}
Embebido de fonts (paraweasyprint)
@font-face {
font-family: 'MiFont';
src: url('fonts/mifont.woff2') format('woff2');
}
Si las fonts no están embebidas, weasyprint usa fallback del sistema.
Limitaciones de weasyprint
| Feature | Soporte | Workaround |
|---|
| HTML/CSS layout | ✅ | — |
| Tablas | ✅ | — |
| CSS @page | ✅ | — |
| Imágenes base64 | ✅ | — |
| Canvas2D charts | ❌ | Exportar como PNG primero |
| Leaflet maps | ❌ | Rasterizar con html2canvas |
| JavaScript | ❌ | Ejecutar JS antes, embebido resultado |
| Web fonts | ⚠️ | Embebidas en CSS @font-face |
| CSS Grid | ⚠️ | Usar Flexbox o tablas como fallback |
| CSS custom props | ⠼ | Resolver antes de exportar |
Pitfalls críticos
- weasyprint NO ejecuta JavaScript — todo contenido dinámico debe estar resuelto en el HTML estático ANTES de la conversión
- Canvas vacío en PDF — los
<canvas> se renderizan como cuadros blancos. SIEMPRE rasterizar antes.
- Maps vacíos en PDF — Leaflet no carga tiles en weasyprint. SIEMPRE usar imagen estática.
- Tamaño del PDF — Un HTML de 164KB con imágenes base64 puede generar un PDF de 1-5MB. Normal para docs de 60+ págs.
- @page margins — weasyprint respeta @page pero los márgenes se aplican a CADA página. Incluir márgenes en el CSS del body también.
- Tabla grande sin page-break-inside: avoid — Las tablas largas se rompen en medio de una fila. Usar
page-break-inside: avoid o avoid-column.
Verificación del output
ls -la output.pdf
python3 -c "import fitz; print(fitz.open('output.pdf').page_count)"
python3 -c "
import fitz
doc = fitz.open('output.pdf')
for i in range(min(3, doc.page_count)):
text = doc[i].get_text()[:200]
print(f'Pág {i+1}: {text[:100]}...')
"
Alternativa: Puppeteer (si está disponible)
Si Chromium/Puppeteer está instalado, es MEJOR que weasyprint:
- Renderiza JavaScript completo
- Charts y maps se renderizan nativamente
- Mejor soporte CSS moderno
- PDF con vectorial real (no rasterizado)
npm install puppeteer
node -e "
const puppeteer = require('puppeteer');
(async () => {
const browser = await puppeteer.launch();
const page = await browser.newPage();
await page.goto('file:///path/to/report.html', {waitUntil: 'networkidle0'});
await page.pdf({path: 'output.pdf', format: 'A4', printBackground: true});
await browser.close();
})();
"
Seguridad: remover API proxy de repos públicos
PITFALL CRÍTICO: Si tu repo es público (GitHub Pages, etc.), nunca incluyas un proxy de API en el código fuente. Exponer server.mjs o similar en un repo público revela infraestructura interna.
Checklist de limpieza:
- Buscar cualquier archivo de servidor (
server.mjs, server.js, proxy.mjs)
- Buscar referencias a URLs internas (
nan.builders, localhost, IPs privadas)
- Si el repo es público → eliminar el proxy, mover a CI/CD o infraestructura separada
- API keys → solo en
localStorage del usuario, nunca en código
Lección de PLANDEMOVILIDAD: Un proxy Node.js (server.mjs) que exponía api.nan.builders/v1 fue eliminado del repo público. La solución: eliminar proxy, usar fallback estático en informe.js, y dejar que la IA se conecte directamente desde el navegador del usuario (sin proxy).
Referencias
references/browser-to-pdf-patterns.md — Transferencia browser→disco detallada
references/plandemovilidad-pdf-pipeline.md — Métricas reales, casos de estudio (report.js vs ia-generativa), seguridad API proxy
- Skill
vanilla-js-dashboard-patterns — Patrones de arquitectura dashboard vanilla JS
- Skill
pdf-processing — Extracción y procesamiento de PDFs (dirección opuesta)