@xenterprises/fastify-xpdf
v1.3.0
Published
Fastify plugin for PDF generation and manipulation. Convert HTML/Markdown to PDF, fill forms, and merge PDFs.
Readme
@xenterprises/fastify-xpdf
Fastify 5 plugin for PDF generation and manipulation — convert HTML/Markdown/URLs to PDF with Puppeteer, fill PDF forms, merge PDFs, and extract pages with pdf-lib, with optional S3-compatible storage via xStorage. For services that need to produce invoices, reports, contracts, and filled forms from a single fastify.xPdf namespace.
Install
npm install @xenterprises/fastify-xpdf fastify@5Peer dependencies: fastify@^5 (required), @xenterprises/fastify-xstorage (optional — only needed when calling methods with saveToStorage: true).
Installing downloads a Chrome build via Puppeteer. Contributors who only run the test suite can skip it:
PUPPETEER_SKIP_DOWNLOAD=1 npm install— the tests never launch Chrome.
Minimal example
import Fastify from "fastify";
import xPDF from "@xenterprises/fastify-xpdf";
const fastify = Fastify();
await fastify.register(xPDF);
const result = await fastify.xPdf.generateFromHtml("<h1>Hello World</h1>");
// result: { buffer, filename, size }All options have defaults; everything is configured through the register options object — the plugin never reads process.env.
Options
| Name | Type | Default | Required | Description |
|------|------|---------|----------|-------------|
| headless | boolean | true | No | Run the Puppeteer browser in headless mode |
| args | string[] | ["--no-sandbox", "--disable-setuid-sandbox"] | No | Chrome/Chromium launch arguments |
| browserFactory | function | puppeteer.launch | No | Injectable browser factory (launchOptions) => Promise<browser>. Receives { headless, args }; used by tests to substitute a fake browser without launching Chrome |
| 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) |
Invalid option types fail fast at registration — see Error behavior.
Decorators
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 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 (returns null when xStorage is not registered; upload errors propagate) |
Error behavior
Registration fails fast on invalid option types. Messages name the plugin, the option, and an example:
xpdf: option `headless` must be a boolean, e.g. `app.register(xPDF, { headless: true })`
xpdf: option `args` must be an array of strings, e.g. `app.register(xPDF, { args: ["--no-sandbox"] })`
xpdf: option `defaultFolder` must be a string, e.g. `app.register(xPDF, { defaultFolder: "pdfs" })`
xpdf: option `format` must be a string, e.g. `app.register(xPDF, { format: "A4" })`
xpdf: option `printBackground` must be a boolean, e.g. `app.register(xPDF, { printBackground: false })`
xpdf: option `margin` must be an object with top/right/bottom/left strings, e.g. `app.register(xPDF, { margin: { top: "1cm", ... } })`
xpdf: option `browserFactory` must be a function returning a Puppeteer-compatible browser, e.g. `app.register(xPDF, { browserFactory: async () => puppeteer.launch() })`Method calls throw real Error objects prefixed with [xPDF]:
| Error | When |
|-------|------|
| [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 |
| xpdf: saveToStorage requires the xStorage plugin — register @xenterprises/fastify-xstorage first or pass saveToStorage: false | saveToStorage: true passed but @xenterprises/fastify-xstorage is not registered |
Storage upload failures (when xStorage is registered) propagate the original xStorage error to the caller; they are logged message-only and never swallowed.
How it works
The plugin uses two engines:
Puppeteer (Chrome headless) for HTML/Markdown/URL to PDF generation. A single browser instance is lazily created on the first generation call via the (injectable)
browserFactoryand reused across requests. It auto-reconnects if the browser disconnects. The browser is closed on Fastify shutdown via theonClosehook. Pages are opened per call and always closed in afinallyblock.pdf-lib for all PDF manipulation (form filling, merging, page extraction, metadata). These operations are pure JavaScript with no browser dependency.
When @xenterprises/fastify-xstorage is registered, any method result can be uploaded to S3-compatible storage by passing saveToStorage: true on that call (with an optional folder override). Passing saveToStorage: true without xStorage registered throws an actionable error — saving is gated per call, there is no plugin-level storage toggle.
The plugin registers itself as xpdf via fastify-plugin with a fastify: "5.x" constraint, so it can be registered in any scope.
Testing
The test suite is fully offline: it never launches or downloads Chrome and needs no env vars. The Puppeteer path is tested against a fake browser injected through the browserFactory option; the pdf-lib paths run against real PDF buffers generated in-test. To install dependencies without downloading Chrome: PUPPETEER_SKIP_DOWNLOAD=1 npm install.
npm test # node --test
npm run lint # biome check
npm run format # biome format --writeRequirements
- Node.js >= 20
- Fastify ^5
License
Proprietary — All Rights Reserved X Enterprises. See LICENSE.
