crystal-node-runtime

Engines de PDF

Todo relatório migrado precisa de um engine para transformar o HTML Vash em PDF.

Vantagem central: a API de entrada é a mesma em todos — trocar de engine é trocar o require, sem reescrever o controller:

await reportEngine.renderReportPdf(model, res, {
  reportDir: __dirname,
  pdf: { /* margens, página, etc. */ }
});

Escolha o engine conforme o ambiente da aplicação hospedeira. As dependências não vêm embutidas neste pacote: instale-as na sua app.

Comparativo

Engine Módulo Quando usar Status
Devbox report-engine-devbox App com adapter res.pdf Pronto
Puppeteer report-engine-puppeteer Node genérico / Chromium Pronto
wkhtmltopdf report-engine-wkhtmltopdf Binário wkhtmltopdf no PATH Pronto
WeasyPrint report-engine-weasyprint Python 3 + WeasyPrint Pronto

Dependências por engine

Comum a Puppeteer e wkhtmltopdf

Dependência Tipo Instalação Link
vash pacote npm npm install vash https://www.npmjs.com/package/vash

Templates report.html / report_header.html / report_footer.html são compilados com Vash dentro desses engines.

Devbox (report-engine-devbox)

Dependência Tipo Como obter
res.pdf API do framework hospedeiro Já existe em apps Devbox / stacks com esse adapter
templates Vash no framework em geral o próprio res.pdf renderiza Vash

Não exige puppeteer nem o binário wkhtmltopdf.

# apenas o runtime CrystalNode
npm install @crystal-node/runtime
const reportEngine = require('@crystal-node/runtime/report-engine-devbox');

Puppeteer (report-engine-puppeteer)

Dependência Tipo Instalação Link
puppeteer pacote npm (baixa Chromium) npm install puppeteer https://www.npmjs.com/package/puppeteer
vash pacote npm npm install vash https://www.npmjs.com/package/vash
# individual
npm install @crystal-node/engine-puppeteer @crystal-node/controller puppeteer vash
# ou meta-pacote
npm install @crystal-node/runtime puppeteer vash

Em CI/Docker, o Puppeteer pode precisar de libs do sistema (fonte, sandbox). Consulte:
https://pptr.dev/troubleshooting

Alternativa mais leve (Chromium externo): puppeteer-core — nesse caso você precisa passar o browser/executablePath via opções do engine (puppeteer / puppeteerLaunch). Link: https://www.npmjs.com/package/puppeteer-core

const reportEngine = require('@crystal-node/runtime/report-engine-puppeteer');

await reportEngine.renderReportPdf(model, res, {
  reportDir: __dirname,
  pdf: {
    marginTop: 28,
    marginBottom: 14,
    marginLeft: 0,
    marginRight: 0,
    pageWidth: '215.9mm',
    pageHeight: '279.4mm'
  },
  // opcional: escala se o HTML veio com outro coeficiente de twips
  // twipsToPxCoefficient: 1.25,
  // debug: true
});

Debug:

REPORT_ENGINE_PUPPETEER_DEBUG=1

Artefatos em <reportDir>/puppeteer-debug/<timestamp>/.

wkhtmltopdf (report-engine-wkhtmltopdf)

Dependência Tipo Instalação Link
wkhtmltopdf binário do sistema (não é pacote npm obrigatório) instalar no SO / PATH https://wkhtmltopdf.org/downloads.html
vash pacote npm npm install vash https://www.npmjs.com/package/vash
npm install @crystal-node/engine-wkhtmltopdf @crystal-node/controller vash
# ou: npm install @crystal-node/runtime vash
# + instalar o binário wkhtmltopdf no sistema

Caminho do binário (opcional):

const reportEngine = require('@crystal-node/runtime/report-engine-wkhtmltopdf');

await reportEngine.renderReportPdf(model, res, {
  reportDir: __dirname,
  pdf: {
    marginTop: 28,
    marginBottom: 14,
    marginLeft: 0,
    marginRight: 0,
    pageWidth: '215.9mm',
    pageHeight: '279.4mm',
    dpi: 96
  },
  // wkhtmltopdfPath: '/usr/local/bin/wkhtmltopdf',
  // debug: true
});

Pacotes npm auxiliares (opcionais, se preferir gerenciar via npm):
https://www.npmjs.com/package/wkhtmltopdf — este runtime chama o binário diretamente; o wrapper npm não é obrigatório.

WeasyPrint (report-engine-weasyprint)

Dependência Tipo Link
WeasyPrint CLI Python (weasyprint ou py -m weasyprint) https://doc.courtbouillon.org/weasyprint/
vash pacote npm (render dos templates) https://www.npmjs.com/package/vash

Status: implementado. Header/footer via position: running() + @page margins; numeração com counter(page) / counter(pages). Binário: options.weasyprintPathWEASYPRINT_PATHweasyprint → fallback python -m weasyprint.

const reportEngine = require('@crystal-node/runtime/report-engine-weasyprint');
await reportEngine.renderReportPdf(model, res, {
  reportDir: __dirname,
  pdf: { marginTop: 26, marginBottom: 30, pageWidth: '210mm', pageHeight: '297mm' },
  weasyprintPath: process.env.WEASYPRINT_PATH // opcional
});

Trocar de engine no controller

A troca é só o require — o restante do controller permanece igual:

// Pacotes individuais (recomendado)
const reportEngine = require('@crystal-node/engine-devbox/report-engine-devbox');
const reportEngine = require('@crystal-node/engine-puppeteer');
const reportEngine = require('@crystal-node/engine-wkhtmltopdf');
const reportEngine = require('@crystal-node/engine-weasyprint');

// Ou via meta-pacote / subpaths (compat 1.0)
const reportEngine = require('@crystal-node/runtime/report-engine-devbox');
const reportEngine = require('@crystal-node/runtime/report-engine-puppeteer');
const reportEngine = require('@crystal-node/runtime/report-engine-wkhtmltopdf');
const reportEngine = require('@crystal-node/runtime/report-engine-weasyprint');

Também disponível pelo entrypoint principal:

const {
  reportEngineDevbox,
  reportEnginePuppeteer,
  reportEngineWkhtmltopdf,
  reportEngineWeasyprint
} = require('@crystal-node/runtime');

Se report_header.html / report_footer.html existirem e tiverem conteúdo, os engines os usam automaticamente.

Numeração de página nos templates (marcadores compatíveis):

@{
  crystalFunctions = Object.assign({}, crystalFunctions, {
    pageNumber: function () { return crystalFunctions.pdfPageChrome('page'); },
    totalPageCount: function () { return crystalFunctions.pdfPageChrome('sitepages'); }
  });
}

Suppress condicional por página (data-cr-suppress-if)

Secções de page header/footer cuja fórmula Crystal cita PageNumber / TotalPageCount saem com data-cr-suppress-if="page >= …" (sem @if Vash). Avaliação:

Engine Mecanismo
wkhtmltopdf / devbox Script no HTML + query ?page= / ?topage=
Puppeteer / WeasyPrint pdf-chrome-suppress.js filtra por página; faixas + merge via pdf-lib

Instale pdf-lib no projeto hospedeiro se for usar Puppeteer ou WeasyPrint com esses marcadores.

Qual escolher na migração manual?

  1. Já está em Devboxreport-engine-devbox
  2. App Node nova / sem res.pdfreport-engine-puppeteer (mais fácil de instalar via npm)
  3. Infra já tem wkhtmltopdfreport-engine-wkhtmltopdf
  4. Fidelidade tipográfica / CSSreport-engine-weasyprint (CLI WeasyPrint + peer pdf-lib se houver suppress por página)