# @xenterprises/fastify-xpdf

Fastify plugin for PDF generation and manipulation — convert HTML/Markdown to PDF, fill PDF forms, merge PDFs, and extract pages, with optional S3-compatible storage via xStorage.

## Install

```bash
npm install @xenterprises/fastify-xpdf
```

**Peer dependency (optional):** `@xenterprises/fastify-xstorage` — required only if you enable `useStorage: true`.

## Usage

```js
import Fastify from "fastify";
import xPDF from "@xenterprises/fastify-xpdf";

const fastify = Fastify();

await fastify.register(xPDF, {
  format: "A4",
  printBackground: true,
  margin: { top: "1cm", right: "1cm", bottom: "1cm", left: "1cm" },
});

// Generate PDF from HTML
const result = await fastify.xPDF.generateFromHtml("<h1>Hello World</h1>");
console.log(result.buffer); // PDF Buffer
console.log(result.size);   // File size in bytes

// Generate from Markdown
const md = await fastify.xPDF.generateFromMarkdown("# Title\n\nParagraph");

// Fill a PDF form
const filled = await fastify.xPDF.fillForm(pdfBuffer, {
  firstName: "Jane",
  agreeToTerms: true,
});

// Merge multiple PDFs
const merged = await fastify.xPDF.mergePDFs([pdf1, pdf2, pdf3]);

await fastify.close();
```

## Options

| Name | Type | Default | Required | Description |
|------|------|---------|----------|-------------|
| `headless` | `boolean` | `true` | No | Run Puppeteer browser in headless mode |
| `args` | `string[]` | `["--no-sandbox", "--disable-setuid-sandbox"]` | No | Chrome/Chromium launch arguments |
| `useStorage` | `boolean` | `false` | No | Enable automatic saving to xStorage |
| `defaultFolder` | `string` | `"pdfs"` | No | Default storage folder for saved PDFs |
| `format` | `string` | `"A4"` | No | Default page format (A4, Letter, A3, A5, Tabloid, Ledger, Legal, A0–A6) |
| `printBackground` | `boolean` | `true` | No | Print background graphics and colors by default |
| `margin` | `object` | `{ top: "1cm", right: "1cm", bottom: "1cm", left: "1cm" }` | No | Default page margins (CSS units) |

## Decorated Properties

### `fastify.xPDF`

All methods are available on the `fastify.xPDF` decorator.

#### `generateFromHtml(html, options?)`

Generate a PDF from HTML content.

| Param | Type | Description |
|-------|------|-------------|
| `html` | `string` | HTML content (required, non-empty) |
| `options.filename` | `string` | Output filename (auto-generated if omitted) |
| `options.format` | `string` | Page format override |
| `options.landscape` | `boolean` | Landscape orientation |
| `options.margin` | `object` | Page margins override |
| `options.printBackground` | `boolean` | Print background override |
| `options.displayHeaderFooter` | `boolean` | Enable header/footer |
| `options.headerTemplate` | `string` | Header HTML template |
| `options.footerTemplate` | `string` | Footer HTML template |
| `options.saveToStorage` | `boolean` | Save to xStorage |
| `options.folder` | `string` | Storage folder override |

**Returns:** `{ buffer, filename, size, storageKey?, url? }`

#### `generateFromMarkdown(markdown, options?)`

Convert Markdown to PDF. Accepts the same options as `generateFromHtml`. The Markdown is parsed with [marked](https://github.com/markedjs/marked) and wrapped in a styled HTML template.

#### `generateFromUrl(url, options?)`

Navigate to a URL and generate a PDF of the rendered page.

| Param | Type | Description |
|-------|------|-------------|
| `url` | `string` | URL to render (required, must be valid) |
| `options.waitFor` | `string` | Puppeteer waitUntil value (default: `"networkidle2"`) |
| `options.timeout` | `number` | Navigation timeout in ms (default: `30000`) |

Plus all options from `generateFromHtml`.

#### `fillForm(pdfBuffer, fieldValues, options?)`

Fill PDF form fields (text, checkbox, radio, dropdown).

| Param | Type | Description |
|-------|------|-------------|
| `pdfBuffer` | `Buffer` | Source PDF with form fields |
| `fieldValues` | `object` | `{ fieldName: value }` key-value pairs |
| `options.flatten` | `boolean` | Flatten form after filling (default: `true`) |
| `options.filename` | `string` | Output filename |
| `options.saveToStorage` | `boolean` | Save to xStorage |
| `options.folder` | `string` | Storage folder |

**Returns:** `{ buffer, filename, size, storageKey?, url? }`

#### `listFormFields(pdfBuffer)`

List all form fields in a PDF.

**Returns:** `[{ name, type, value }]` — type is one of: `text`, `checkbox`, `radio`, `dropdown`, `option`, `button`, `signature`, `unknown`.

#### `mergePDFs(pdfBuffers, options?)`

Merge multiple PDFs into a single document.

| Param | Type | Description |
|-------|------|-------------|
| `pdfBuffers` | `Buffer[]` | Array of PDF buffers (required, non-empty) |
| `options.filename` | `string` | Output filename |
| `options.saveToStorage` | `boolean` | Save to xStorage |
| `options.folder` | `string` | Storage folder |

**Returns:** `{ buffer, filename, size, pageCount, storageKey?, url? }`

#### `extractPages(pdfBuffer, pageIndices, options?)`

Extract specific pages from a PDF into a new document.

| Param | Type | Description |
|-------|------|-------------|
| `pdfBuffer` | `Buffer` | Source PDF buffer |
| `pageIndices` | `number[]` | Zero-based page indices to extract |
| `options.filename` | `string` | Output filename |
| `options.saveToStorage` | `boolean` | Save to xStorage |
| `options.folder` | `string` | Storage folder |

**Returns:** `{ buffer, filename, size, pageCount, storageKey?, url? }`

#### `getPageCount(pdfBuffer)`

Get the number of pages in a PDF. Returns a `number`.

#### `getMetadata(pdfBuffer)`

Get PDF metadata.

**Returns:** `{ pageCount, title, author, subject, creator, creationDate, modificationDate, size }`

## Exported Helpers

Available via `import { helpers } from "@xenterprises/fastify-xpdf/helpers"`:

| Helper | Description |
|--------|-------------|
| `generatePdfFilename(baseName?)` | Generate unique filename with timestamp |
| `isValidPdfBuffer(buffer)` | Check if buffer starts with `%PDF` header |
| `getPdfMetadata(buffer)` | Get `{ size }` from a PDF buffer |
| `formatPdfOptions(options, defaults)` | Merge options with defaults (deep-merges margin) |
| `sanitizeFilename(filename)` | Remove unsafe characters, lowercase |
| `wrapHtmlTemplate(content)` | Wrap HTML fragment in a full styled document |
| `parseMargin(margin)` | Convert string or object margin to Puppeteer format |
| `getPageFormat(format?)` | Get `{ width, height }` in inches for a format name |
| `saveToStorage(fastify, buffer, filename, folder)` | Upload PDF to xStorage if available |

## Environment Variables

| Name | Required | Description |
|------|----------|-------------|
| `PUPPETEER_HEADLESS` | No | `true`/`false` — headless mode (default: `true`) |
| `PUPPETEER_ARGS` | No | Comma-separated Chrome args |
| `PDF_DEFAULT_FORMAT` | No | Default page format (default: `A4`) |
| `PDF_USE_STORAGE` | No | Enable xStorage integration (default: `false`) |
| `PDF_DEFAULT_FOLDER` | No | Default storage folder (default: `pdfs`) |

## Error Reference

All errors are prefixed with `[xPDF]`.

| Error | When |
|-------|------|
| `[xPDF] 'headless' option must be a boolean` | Invalid headless option at startup |
| `[xPDF] 'args' option must be an array of strings` | Invalid args option at startup |
| `[xPDF] 'useStorage' option must be a boolean` | Invalid useStorage option at startup |
| `[xPDF] 'defaultFolder' option must be a string` | Invalid defaultFolder option at startup |
| `[xPDF] 'format' option must be a string` | Invalid format option at startup |
| `[xPDF] 'printBackground' option must be a boolean` | Invalid printBackground option at startup |
| `[xPDF] 'margin' option must be an object...` | Invalid margin option at startup |
| `[xPDF] HTML content must be a non-empty string` | Empty/null HTML passed to generateFromHtml |
| `[xPDF] Markdown content must be a non-empty string` | Empty/null markdown passed to generateFromMarkdown |
| `[xPDF] URL must be a non-empty string` | Empty/null URL passed to generateFromUrl |
| `[xPDF] URL must be a valid URL` | Invalid URL format |
| `[xPDF] Invalid PDF buffer` | Non-PDF buffer passed to form/merge/extract methods |
| `[xPDF] fieldValues must be an object` | Non-object passed as fieldValues to fillForm |
| `[xPDF] pdfBuffers must be a non-empty array...` | Empty/null array passed to mergePDFs |
| `[xPDF] One or more invalid PDF buffers provided` | Invalid buffer in merge array |
| `[xPDF] pageIndices must be a non-empty array...` | Invalid pageIndices in extractPages |
| `[xPDF] Each page index must be a non-negative integer` | Non-integer or negative page index |
| `[xPDF] Page index N out of range...` | Page index exceeds PDF page count |
| `[xPDF] Failed to initialize PDF browser` | Puppeteer browser launch failure |
| `[xPDF] Failed to process PDF during merge` | Corrupt PDF during merge operation |

## How It Works

The plugin uses two engines:

1. **Puppeteer** (Chrome headless) for HTML/Markdown/URL to PDF generation. A single browser instance is lazily initialized on the first generation call and reused across requests. It auto-reconnects if the browser disconnects. The browser is closed on Fastify shutdown via the `onClose` hook.

2. **pdf-lib** for all PDF manipulation (form filling, merging, page extraction, metadata). These operations are pure JavaScript with no browser dependency.

When `useStorage: true` and `@xenterprises/fastify-xstorage` is registered, generated PDFs can be automatically uploaded to S3-compatible storage. The `saveToStorage` option on each method call controls whether that specific result is uploaded.

The plugin registers itself as `xPDF` via `fastify-plugin` with a `fastify >= 5.0.0` constraint and no dependencies (it can be registered in any scope).
