Skip to content

Plataforma técnica · Orchestrator

Pdf Service

Orchestrator Common Pdf

PdfService es un wrapper delgado sobre Puppeteer que produce Buffer PDF a partir de una URL o de HTML inline. Métodos estáticos — no se instancia.

  • Generar PDFs de liquidaciones de sueldo (lo más común).
  • Exportar reportes contables.
  • Producir contratos finiquitos.

Para generación masiva o templating complejo, los servicios consumidores combinan docxtemplater/pizzip con PdfService.generateFromHtml. El service solo se encarga del paso “HTML/URL → PDF”.

static async generateFromUrl(url: string, options?: PdfServiceOptions): Promise<Buffer>

Lanza un browser headless, navega a url con waitUntil: 'networkidle0' (todo lo HTTP terminado por al menos 500ms), emula screen y exporta a PDF.

Útil cuando hay un endpoint que renderiza el documento (e.g. una página de previsualización servida por Sevastopol o el propio Orchestrator).

PdfService.generateFromHtml(html, options?, baseUrl?)

Section titled “PdfService.generateFromHtml(html, options?, baseUrl?)”
static async generateFromHtml(
html: string,
options?: PdfServiceOptions,
baseUrl?: string,
): Promise<Buffer>

Setea el HTML con setContent(... { waitUntil: 'networkidle0' }). Si se pasa baseUrl y el HTML no contiene <base ...>, inyecta uno dentro de <head> para resolver links/imgs relativos.

Útil cuando ya tienes el HTML armado (Mustache, plantilla, string concat) y no necesitas una URL accesible.

PdfServiceOptions extiende un subset de PDFOptions de Puppeteer:

CampoDefaultNotas
format'Letter''A4', 'Legal', etc.
printBackgroundtrueImprime backgrounds CSS.
preferCSSPageSizetrue@page del CSS gana sobre format.
margin{ top, right, bottom, left: '1cm' }Override individual.
scale— (Puppeteer default 1)Útil para encoger reportes anchos.
landscape— (Puppeteer default false)
Uso típico desde un router
import { PdfService } from '@/domain/common/PdfService';
router.get('/api/remuneraciones/liquidaciones/:id/pdf', authenticateToken, asyncHandler(async (req, res) => {
const html = await renderLiquidacionHtml(req.params.id);
const buffer = await PdfService.generateFromHtml(html, {
format: 'Letter',
margin: { top: '0.8cm', right: '0.8cm', bottom: '0.8cm', left: '0.8cm' },
});
res.setHeader('Content-Type', 'application/pdf');
res.setHeader('Content-Disposition', `attachment; filename="liquidacion-${req.params.id}.pdf"`);
res.send(buffer);
}));

Cada llamada:

  1. puppeteer.launch({ args: ['--no-sandbox', '--disable-setuid-sandbox'] }).
  2. browser.newPage().
  3. Navegación / setContent + emulateMediaType('screen').
  4. page.pdf(...).
  5. browser.close() en finally.

--no-sandbox es necesario para correr Chromium en Docker / Cloud Run sin permisos de privilegio. Si se ejecuta en un host hardened, considerar quitarlo y configurar el sandbox de Chromium correctamente.

Browser por llamada (no reusado) — cada generateFromX arranca y cierra Chromium. Esto es caro (~500ms-1s de arranque). Para alto throughput, considerar:

  1. Browser pool reusado (puppeteer-cluster o equivalente custom).
  2. Worker pool dedicado para PDFs.
  3. Pre-warmed browser singleton.

Ninguna optimización está implementada — la frecuencia actual (liquidaciones bajo demanda) no la justifica.

Por default Puppeteer usa print que aplica @media print CSS. El service fuerza screen para que el PDF se vea como el browser lo muestra. Si tu plantilla quiere un look específico para print, configurar @media print y NO llamar emulateMediaType — pero eso requeriría modificar PdfService (no hay opción para suprimirlo).

Ambos métodos esperan networkidle0 (no requests en flight por 500ms). Para páginas con polling continuo (WebSocket abierto, telemetría, beacons), esto puede colgarse. Si encuentras timeout en una página específica, considerar:

  • Servir una versión print-only sin polling.
  • Cambiar a networkidle2 (≤2 conexiones activas) modificando el service.

PdfService solo retorna el Buffer. La persistencia (e.g. guardar en storage/liquidaciones/) la decide el caller. La convención actual:

import path from 'path';
import fs from 'fs/promises';
const dir = process.env.LIQUIDACIONES_DIR ?? './storage/liquidaciones';
const filePath = path.join(dir, `${liquidacionId}.pdf`);
await fs.writeFile(filePath, buffer);

Servido luego por /files/liquidaciones/<id>.pdf con Cache-Control: public, max-age=31536000, immutable (ver Orchestrator › Static files).