# HTML To PDF Report Pipeline

> 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.

- Skill: `ntizar/html-to-pdf-report-pipeline` (Agent Skill, multi-file: 2 files)
- Install (CLI): `npx skillmds@latest add ntizar/html-to-pdf-report-pipeline`
- Raw SKILL.md: https://api.skillmd.com/api/skills/ntizar/html-to-pdf-report-pipeline/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Web & Frontend
- Author: ntizar (https://skillmd.com/u/ntizar)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/ntizar/html-to-pdf-report-pipeline

---


# 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

```js
// En browser, tras cargar datos y sincronizar appState
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:
```js
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)

```js
// Convertir todos los canvas a imágenes ANTES de exportar
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)

```js
// Opción 1: html2canvas (si está disponible)
const mapEl = document.getElementById('map');
const canvas = await html2canvas(mapEl);
const img = document.createElement('img');
img.src = canvas.toDataURL();
mapEl.parentNode.replaceChild(img, mapEl);

// Opción 2: exportMapAsImage() del proyecto (usa html2canvas lazy-loaded)
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

```bash
weasyprint input.html output.pdf
```

### CSS esencial para A4

```css
@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)

```css
@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

1. **weasyprint NO ejecuta JavaScript** — todo contenido dinámico debe estar resuelto en el HTML estático ANTES de la conversión
2. **Canvas vacío en PDF** — los `<canvas>` se renderizan como cuadros blancos. SIEMPRE rasterizar antes.
3. **Maps vacíos en PDF** — Leaflet no carga tiles en weasyprint. SIEMPRE usar imagen estática.
4. **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.
5. **@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.
6. **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

```bash
# Tamaño mínimo esperado para doc profesional
ls -la output.pdf
# Debe ser >500KB para 60+ págs con imágenes

# Conteo de páginas (si tienes PyMuPDF)
python3 -c "import fitz; print(fitz.open('output.pdf').page_count)"

# Verificar que no está vacío
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)

```bash
# Instalar Puppeteer
npm install puppeteer

# Script de conversión
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:**
1. Buscar cualquier archivo de servidor (`server.mjs`, `server.js`, `proxy.mjs`)
2. Buscar referencias a URLs internas (`nan.builders`, `localhost`, IPs privadas)
3. Si el repo es público → eliminar el proxy, mover a CI/CD o infraestructura separada
4. 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)

