npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

safeer-pdf-generator

v1.3.7

Published

Framework-agnostic PDF generation library with chunking, merging, S3 upload, and email delivery

Downloads

802

Readme

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: '[email protected]',
    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: '[email protected]', ... },        // 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

🔗 Links