safeer-pdf-generator
v1.3.7
Published
Framework-agnostic PDF generation library with chunking, merging, S3 upload, and email delivery
Downloads
802
Maintainers
Readme
safeer-pdf-generator
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 Templates —
default,simple, andmodernwith 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-browserPackage 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 viapuppeteer.browserInstancethrowsbrowser.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:processedrequires>= 1.3.4.file:savedrequires>= 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-...pdfCombine 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.pathvalue is used verbatim withfs.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 hadreturn: 'file'andoutputPathfields declared in the type, but neither was implemented — they are now marked@deprecatedin favor oflocalFs.
🏗️ 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
