# safeer-pdf-generator

[![npm version](https://badge.fury.io/js/safeer-pdf-generator.svg)](https://badge.fury.io/js/safeer-pdf-generator)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
[![Downloads](https://img.shields.io/npm/dm/safeer-pdf-generator.svg)](https://www.npmjs.com/package/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`, 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:

```bash
# Install Chromium
sudo apt-get install chromium-browser

# Or set custom path
export PUPPETEER_EXECUTABLE_PATH=/usr/bin/chromium-browser
```

### Package Installation

```bash
npm install safeer-pdf-generator

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

## 🚀 Quick Start

```typescript
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:

```typescript
// 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:

```typescript
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:

```typescript
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:

```typescript
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:

```typescript
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:

```typescript
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:

```typescript
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

```typescript
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:

```typescript
// 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:

```typescript
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:

```typescript
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

```typescript
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

```typescript
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

```typescript
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

```typescript
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](examples/simple-usage.js) | Basic PDF generation |
| [custom-template.js](examples/custom-template.js) | Custom CSS, inline templates, and registry |
| [byob-and-events.js](examples/byob-and-events.js) | Browser reuse + lifecycle events |
| [local-fs-output.js](examples/local-fs-output.js) | Local disk output + `chunk:processed` progress events |
| [webhook-integration.js](examples/webhook-integration.js) | Webhook dispatch with HMAC signing |
| [express-app/](examples/express-app/) | Full Express.js integration |
| [nestjs-simplified/](examples/nestjs-simplified/) | NestJS service example |

## 📄 License

MIT © [Safeersoft](https://github.com/Safeersoft)

## 🔗 Links

- [NPM Package](https://www.npmjs.com/package/safeer-pdf-generator)
- [GitHub Repository](https://github.com/Safeersoft/pdf-reporter)
- [Issues & Support](https://github.com/Safeersoft/pdf-reporter/issues)
