npm.io
1.4.0 • Published 2d ago

safeer-pdf-generator

Licence
MIT
Version
1.4.0
Deps
3
Size
21.8 MB
Vulns
0
Weekly
0

safeer-pdf-generator

npm version License: MIT Downloads

A powerful, framework-agnostic TypeScript library for generating PDF reports with chunking, merging, S3 upload, email delivery, lifecycle events, and webhook dispatch.

Features

  • High Performance — Concurrent chunk processing with memory optimization
  • 3 Built-in Templatesdefault, simple, and modern with full customization
  • Custom Templates — Inline functions, CSS overrides, or register your own
  • Framework Agnostic — Works with Express, NestJS, Fastify, or standalone
  • S3 & S3-Compatible — Direct upload to AWS S3, Cloudflare R2, MinIO, Backblaze B2, etc.
  • Local File Output — Write PDFs to disk alongside S3/email/webhook
  • Email Delivery — Send PDFs as attachments or download links
  • PDF Operations — Merge, split, and manipulate existing PDFs
  • BYOB Browser — Bring Your Own Browser for connection pooling
  • Lifecycle Events — Typed event emitters for every stage (incl. per-chunk progress)
  • Webhook Dispatcher — Auto-POST to your backend with HMAC signing
  • Memory Efficient — Handle large datasets (100k+ records)
  • TypeScript — Full type safety and IntelliSense support

Installation

Prerequisites

This package requires Chromium for PDF generation:

# Install Chromium
sudo apt-get install chromium-browser

# Or set custom path
export PUPPETEER_EXECUTABLE_PATH=/usr/bin/chromium-browser
Package Installation
npm install safeer-pdf-generator

# Optional peer dependencies
npm install @aws-sdk/client-s3 nodemailer

Quick Start

import { generatePdf } from 'safeer-pdf-generator';

const result = await generatePdf({
  title: 'Sales Report',
  data: [
    { name: 'John Doe', sales: 1200, region: 'North' },
    { name: 'Jane Smith', sales: 1500, region: 'South' },
  ],
  columns: [
    { key: 'name', title: 'Name', dataIndex: 'name' },
    { key: 'sales', title: 'Sales', dataIndex: 'sales' },
    { key: 'region', title: 'Region', dataIndex: 'region' },
  ],
});

console.log(`PDF: ${result.fileName} (${result.pageCount} pages, ${result.durationMs}ms)`);

Custom Templates

Built-in Templates

Three templates are available out of the box:

// Modern template (gradient header, card-style info)
await generatePdf({ ...options, template: 'modern' });

// Default template (classic table with company header)
await generatePdf({ ...options, template: 'default' });

// Simple template (minimal, lightweight)
await generatePdf({ ...options, template: 'simple' });
Custom CSS & HTML Injection

Override styles and add HTML blocks using any built-in template:

const result = await generatePdf({
  title: 'Styled Report',
  data, columns,
  template: 'modern',
  customCss: `
    .modern-table th {
      background: linear-gradient(135deg, #7c3aed, #2563eb) !important;
      color: #fff !important;
    }
  `,
  customHtml: {
    beforeTable: '<div style="padding: 12px; background: #f5f3ff;">Q4 2025 Summary</div>',
    afterTable: '<div style="font-size: 11px; color: #64748b;">Confidential</div>',
  },
});
Inline Template Function

Full control over the generated HTML:

const result = await generatePdf({
  title: 'Custom Report',
  data, columns,
  template: (params) => {
    const { title, data, columns } = params;

    const html = `
      <!DOCTYPE html>
      <html>
        <head><meta charset="UTF-8"><title>${title}</title></head>
        <body>
          <h1>${title}</h1>
          <table>
            <thead><tr>${columns.map(c => `<th>${c.title}</th>`).join('')}</tr></thead>
            <tbody>
              ${data.map(row =>
                `<tr>${columns.map(c => `<td>${row[c.dataIndex] ?? '—'}</td>`).join('')}</tr>`
              ).join('')}
            </tbody>
          </table>
        </body>
      </html>
    `;

    return { html, header: '', footer: '' };
  },
});
Registered Templates

Register a reusable template globally:

import { registerTemplate, generatePdf } from 'safeer-pdf-generator';

registerTemplate('invoice', (params) => {
  const { title, data, columns } = params;
  // ... return { html, header, footer }
});

// Use it by name anywhere
await generatePdf({ ...options, template: 'invoice' });

BYOB (Bring Your Own Browser)

Reuse a shared Puppeteer browser instance across multiple generations — saves memory and startup time:

import puppeteer from 'puppeteer';
import { generatePdf } from 'safeer-pdf-generator';

const browser = await puppeteer.launch({ headless: true });

// Generate multiple PDFs using the same browser
for (const report of reports) {
  await generatePdf({
    ...report,
    puppeteer: { browserInstance: browser },
  });
}

// You manage the browser lifecycle
await browser.close();

Note: BYOB requires safeer-pdf-generator >= 1.3.3. On 1.3.2 and earlier, passing a Browser instance via puppeteer.browserInstance throws browser.newPage is not a function. Upgrade or pin ^1.3.3.

Lifecycle Events

Listen for typed events during PDF generation:

import { generatePdf, PdfEventEmitter } from 'safeer-pdf-generator';

const events = new PdfEventEmitter();

events.onEvent('generation:started', ({ title, rowCount }) => {
  console.log(`Generating: ${title} (${rowCount} rows)`);
});

// Per-chunk progress — fires once per chunk during chunked generation
events.onEvent('chunk:processed', ({ chunkIndex, totalChunks, sizeBytes }) => {
  const pct = (((chunkIndex + 1) / totalChunks) * 100).toFixed(0);
  console.log(`Chunk ${chunkIndex + 1}/${totalChunks} done (${sizeBytes} bytes) — ${pct}%`);
});

events.onEvent('generation:complete', ({ result }) => {
  console.log(`Done: ${result.pageCount} pages in ${result.durationMs}ms`);
});

events.onEvent('s3:upload:complete', ({ url }) => {
  notifyClient(url);
});

events.onEvent('file:saved', ({ path, sizeBytes }) => {
  console.log(`Wrote ${sizeBytes} bytes to ${path}`);
});

events.onEvent('error', ({ error, phase }) => {
  alertOps(`PDF failed at ${phase}: ${error.message}`);
});

await generatePdf({ ...options, events });

Available events:

Event Payload When
generation:started { title, rowCount, timestamp } Generation begins
chunk:processed { chunkIndex, totalChunks, sizeBytes } Each chunk finishes rendering (chunked path only)
generation:complete { result } Final PDF assembled
s3:upload:complete { url, key, requestPayload? } S3 upload finishes
file:saved { path, sizeBytes } Local file written (when localFs is set)
email:sent { to, messageId? } Email accepted by SMTP server
error { error, phase, stack? } Any phase fails

Note: chunk:processed requires >= 1.3.4. file:saved requires >= 1.3.6. On earlier versions these events were declared in the type map but never fired.

Webhook Dispatcher

Automatically POST to your backend when a PDF is ready. Includes HMAC-SHA256 signing and retry logic:

await generatePdf({
  ...options,
  webhook: {
    url: 'https://api.example.com/webhooks/pdf-ready',
    secret: process.env.WEBHOOK_SECRET,             // HMAC-SHA256 signing
    metadata: { tenantId: 'acme', userId: 'u-123' }, // Pass-through data
    timeoutMs: 10000,
  },
});

// Your endpoint receives:
// POST { s3Url, title, fileName, sizeBytes, pageCount, durationMs, metadata }
// Headers: { X-Webhook-Signature: "sha256=abc123..." }

S3 Upload & Email

const result = await generatePdf({
  title: 'Monthly Report',
  data: salesData,
  columns: [
    { key: 'name', title: 'Sales Rep', dataIndex: 'name' },
    { key: 'sales', title: 'Sales', dataIndex: 'sales' },
  ],
  s3: {
    bucket: 'my-pdfs',
    region: 'us-east-1',
    accessKeyId: process.env.AWS_ACCESS_KEY_ID,
    secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY,
  },
  email: {
    to: 'user@example.com',
    attachmentMode: 'link',
    smtp: {
      host: 'smtp.gmail.com',
      port: 587,
      secure: false,
      auth: { user: process.env.SMTP_USER, pass: process.env.SMTP_PASS },
    },
  },
});

console.log(`Uploaded: ${result.s3?.url}`);
S3-Compatible Storage (R2, MinIO, B2, Spaces, Wasabi)

Set the endpoint field. The package uses @aws-sdk/client-s3 under the hood, so any S3-compatible provider works:

// Cloudflare R2
s3: {
  bucket: 'reports',
  region: 'auto',
  endpoint: 'https://<account-id>.r2.cloudflarestorage.com',
  accessKeyId: process.env.R2_ACCESS_KEY_ID,
  secretAccessKey: process.env.R2_SECRET_ACCESS_KEY,
}

// MinIO (self-hosted)
s3: {
  bucket: 'reports',
  region: 'us-east-1',
  endpoint: 'http://minio.internal:9000',
  accessKeyId: process.env.MINIO_ACCESS_KEY,
  secretAccessKey: process.env.MINIO_SECRET_KEY,
}

// Backblaze B2
s3: {
  bucket: 'reports',
  region: 'us-west-002',
  endpoint: 'https://s3.us-west-002.backblazeb2.com',
  accessKeyId: process.env.B2_KEY_ID,
  secretAccessKey: process.env.B2_APP_KEY,
}

Local File Output

Write the generated PDF to local disk. Composes with s3, email, and webhook — every destination runs on the same call:

import { generatePdf } from 'safeer-pdf-generator';

const result = await generatePdf({
  title: 'Sales Report',
  data, columns,
  localFs: {
    path: '/var/reports',           // directory; created if missing
    // filename: 'custom.pdf',      // optional override; defaults to result.fileName
    // createDir: true,             // mkdir -p (default true)
    // overwrite: true,             // when false, throws if target exists
  },
});

console.log(`Saved: ${result.localFs?.path}`);  // /var/reports/Sales-Report-2026-05-17-...pdf

Combine destinations freely — they all run, all populate the result, all emit their own lifecycle event:

await generatePdf({
  title, data, columns,
  localFs:  { path: '/var/archive' },              // local archive copy
  s3:       { bucket: 'cdn', region: 'us-east-1' }, // primary distribution
  email:    { to: 'finance@acme.com', ... },        // notify stakeholders
  webhook:  { url: 'https://api.acme.com/hook' },   // notify backend
});

Security note: The localFs.path value is used verbatim with fs.writeFile. The library does not sandbox it. If the path comes from untrusted input (e.g., an HTTP request body), validate it in your code before passing it in.

Requires >= 1.3.6. Earlier versions had return: 'file' and outputPath fields declared in the type, but neither was implemented — they are now marked @deprecated in favor of localFs.

Framework Examples

Express.js
import express from 'express';
import { generatePdf } from 'safeer-pdf-generator';

const app = express();

app.post('/generate-report', async (req, res) => {
  try {
    const result = await generatePdf({
      title: req.body.title,
      data: req.body.data,
      columns: req.body.columns,
    });

    res.setHeader('Content-Type', 'application/pdf');
    res.setHeader('Content-Disposition', `attachment; filename="${result.fileName}"`);
    res.send(result.buffer);
  } catch (error) {
    res.status(500).json({ error: 'PDF generation failed' });
  }
});
NestJS
import { Injectable } from '@nestjs/common';
import { generatePdf, PdfEventEmitter } from 'safeer-pdf-generator';

@Injectable()
export class PdfService {
  async createReport(data: any[], title: string) {
    const events = new PdfEventEmitter();
    events.onEvent('generation:complete', ({ result }) => {
      this.logger.log(`PDF ready: ${result.fileName}`);
    });

    return generatePdf({
      title,
      data,
      columns: [
        { key: 'id', title: 'ID', dataIndex: 'id' },
        { key: 'name', title: 'Name', dataIndex: 'name' },
      ],
      template: 'modern',
      events,
    });
  }
}

Large Dataset Handling

import { generatePdf } from 'safeer-pdf-generator';

const result = await generatePdf({
  title: 'Large Report',
  data: largeDataset, // 100,000+ records
  columns,
  chunking: {
    enabled: true,
    chunkSize: 500,       // Rows per chunk
    maxConcurrency: 4,    // Parallel chunk processing
  },
});

PDF Merging

import { mergePdfs } from 'safeer-pdf-generator';

const merged = await mergePdfs({
  buffers: [buffer1, buffer2, buffer3],
});

Configuration Reference

PdfGenerationOptions
Option Type Default Description
title string required Report title
data any[] required Array of data rows
columns ColumnDefinition[] required Column definitions
template string | Function 'default' Template name or compiler function
customCss string undefined CSS injected into the template
customHtml { beforeTable?, afterTable? } undefined HTML injected around the table
locale string 'en' Locale ('en', 'ar' for RTL)
userInfo UserInfo undefined User info shown in header
infoSection InfoSection[] undefined Key-value info cards
chunking ChunkingOptions { enabled: true, chunkSize: 100 } Chunk processing config
s3 S3UploadConfig | false false S3 / S3-compatible upload configuration
localFs LocalFsConfig | false false Write PDF to local disk (since 1.3.6)
email EmailSendConfig | false false Email delivery configuration
webhook WebhookConfig | false false Webhook dispatch configuration
events PdfEventEmitter undefined Lifecycle event emitter
puppeteer PuppeteerOptions { headless: true } Puppeteer launch options
puppeteer.browserInstance Browser undefined External browser (BYOB)
hooks HooksConfig undefined Sync lifecycle hooks
logging LoggingAdapter noOpLogger Logger implementation
timeoutMs number 300000 Global timeout (ms)
ColumnDefinition
Field Type Description
key string Unique column identifier
title string Column header text
dataIndex string Property path in data objects (supports nested: 'address.city')
type 'image' | 'boolean' | 'text' | 'link' Optional column type for custom rendering
WebhookConfig
Field Type Default Description
url string required Endpoint URL
secret string undefined HMAC-SHA256 secret
metadata Record<string, any> undefined Pass-through data
timeoutMs number 10000 Request timeout
LocalFsConfig
Field Type Default Description
path string required Destination directory. Relative paths resolve from process.cwd().
filename string result.fileName Override the auto-generated filename.
createDir boolean true Create parent directories recursively if missing.
overwrite boolean true When false, throws LocalFsError if the target exists.

The corresponding result field: result.localFs = { path: string, sizeBytes: number } (absolute path to the written file).

Why Choose safeer-pdf-generator?

  • Production Ready — Used in enterprise applications
  • Memory Efficient — Handles massive datasets without memory issues
  • Developer Friendly — Full TypeScript support with IntelliSense
  • Framework Agnostic — Works with any Node.js framework
  • Full Featured — Templates, S3, email, webhooks, events — all in one package
  • Active Maintenance — Regular updates and community support

Examples

Example Description
simple-usage.js Basic PDF generation
custom-template.js Custom CSS, inline templates, and registry
byob-and-events.js Browser reuse + lifecycle events
local-fs-output.js Local disk output + chunk:processed progress events
webhook-integration.js Webhook dispatch with HMAC signing
express-app/ Full Express.js integration
nestjs-simplified/ NestJS service example

License

MIT Safeersoft