fastify-xpdf
fastify-xpdf
PDF generation and manipulation for Fastify 5. Uses Puppeteer (Chrome headless) for HTML/Markdown/URL rendering and pdf-lib for form filling, merging, page extraction, and metadata — with optional per-call upload to S3-compatible storage via @xenterprises/fastify-xstorage.
Breaking change (1.0): the decorator was renamed from
fastify.xPDFtofastify.xPdf(lowercasedf). Update every call site. The plugin-leveluseStorageoption was also removed — saving to storage is now gated per call viasaveToStorage: true, and passing it without xStorage registered throws an error instead of silently skipping the upload.
Installation
npm install @xenterprises/fastify-xpdf
# Optional — required only when calling methods with saveToStorage: true
npm install @xenterprises/fastify-xstorage
Quick Start
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" },
});
fastify.get("/reports/:id/pdf", async (request, reply) => {
const html = await renderReportHtml(request.params.id);
const { buffer } = await fastify.xPdf.generateFromHtml(html);
return reply
.header("Content-Type", "application/pdf")
.header("Content-Disposition", 'attachment; filename="report.pdf"')
.send(buffer);
});
await fastify.listen({ port: 3000 });
All options have defaults — await fastify.register(xPDF) is a valid minimal registration.
Options
| Name | Type | Default | Required | Description |
|---|---|---|---|---|
headless | boolean | true | No | Run Puppeteer in headless mode |
args | string[] | ["--no-sandbox", "--disable-setuid-sandbox"] | No | Chrome 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, etc.) |
printBackground | boolean | true | No | Print background graphics and colours |
margin | object | { top: "1cm", right: "1cm", bottom: "1cm", left: "1cm" } | No | Default page margins (CSS units) |
Methods
All methods are on fastify.xPdf.
Generation (Puppeteer-based):
- generateFromHtml(html, options?) — Convert an HTML string to PDF.
- generateFromMarkdown(markdown, options?) — Convert a Markdown string to PDF.
- generateFromUrl(url, options?) — Render a live URL to PDF with Puppeteer.
Form operations (pdf-lib):
- fillForm(pdfBuffer, fieldValues, options?) — Fill and optionally flatten PDF form fields.
- listFormFields(pdfBuffer) — Enumerate all form fields in a PDF template.
Manipulation (pdf-lib):
- mergePDFs(pdfBuffers, options?) — Merge multiple PDFs into a single document.
- extractPages(pdfBuffer, pageIndices, options?) — Extract specific pages into a new document.
- getPageCount / getMetadata — Get page count or full metadata (title, author, dates, size).
Exported Helpers
Available via import { helpers } from "@xenterprises/fastify-xpdf/helpers":
| Helper | Description |
|---|---|
generatePdfFilename(baseName?) | Generate a unique filename with timestamp |
isValidPdfBuffer(buffer) | Returns true if buffer starts with %PDF header |
getPdfMetadata(buffer) | Returns { size } from a PDF buffer |
formatPdfOptions(options, defaults) | Merge per-call options with plugin defaults |
sanitizeFilename(filename) | Remove unsafe characters and lowercase |
wrapHtmlTemplate(content) | Wrap an HTML fragment in a full styled document |
parseMargin(margin) | Convert string or object margin to Puppeteer format |
getPageFormat(format?) | Return { width, height } in inches for a format name |
saveToStorage(fastify, buffer, filename, folder) | Upload a PDF buffer to xStorage (returns null when xStorage is not registered; upload errors propagate) |
Error Reference
Registration fails fast on invalid option types. Messages name the plugin, the option, and an example:
| Error | Cause |
|---|---|
xpdf: option `headless` must be a boolean, e.g. `app.register(xPDF, { headless: true })` | Non-boolean headless at registration |
xpdf: option `args` must be an array of strings, e.g. `app.register(xPDF, { args: ["--no-sandbox"] })` | Non-array args at registration |
xpdf: option `defaultFolder` must be a string, e.g. `app.register(xPDF, { defaultFolder: "pdfs" })` | Non-string defaultFolder |
xpdf: option `format` must be a string, e.g. `app.register(xPDF, { format: "A4" })` | Non-string format |
xpdf: option `printBackground` must be a boolean, e.g. `app.register(xPDF, { printBackground: false })` | Non-boolean printBackground |
xpdf: option `margin` must be an object with top/right/bottom/left strings, e.g. ...` | Invalid margin at registration |
xpdf: option `browserFactory` must be a function returning a Puppeteer-compatible browser, e.g. ...` | Non-function browserFactory |
Method calls throw real Error objects prefixed with [xPDF]:
| Error | Cause |
|---|---|
[xPDF] HTML content must be a non-empty string | Empty/null HTML in generateFromHtml |
[xPDF] Markdown content must be a non-empty string | Empty/null markdown |
[xPDF] URL must be a non-empty string | Empty/null URL |
[xPDF] URL must be a valid URL | Malformed URL string |
[xPDF] Invalid PDF buffer | Non-PDF buffer passed to manipulation methods |
[xPDF] fieldValues must be an object | Non-object fieldValues in fillForm |
[xPDF] pdfBuffers must be a non-empty array... | Empty/null array in 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 index |
[xPDF] Page index N out of range... | Index exceeds PDF page count |
[xPDF] Failed to initialize PDF browser | Puppeteer browser launch failure |
[xPDF] Failed to process PDF during merge | Corrupt PDF encountered during merge |
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.
Environment Variables
None. The plugin never reads process.env — every option is passed explicitly via app.register(xPDF, { ... }). If you want environment-driven configuration, read the variables in your own app and pass the values in:
await fastify.register(xPDF, {
format: process.env.PDF_FORMAT ?? "A4",
headless: process.env.PUPPETEER_HEADLESS !== "false",
});
How It Works
The plugin uses two engines. Puppeteer handles HTML, Markdown, and URL-to-PDF generation: a single Chrome browser instance is lazily initialised on the first generation call (via the injectable browserFactory option), reused across requests, and closed via Fastify's onClose hook on shutdown. It auto-reconnects if the browser disconnects. pdf-lib handles all manipulation operations (form filling, merging, page extraction, metadata) in pure JavaScript with no browser. When @xenterprises/fastify-xstorage is registered on the same Fastify instance, any method result can be uploaded to S3-compatible storage by passing saveToStorage: true on that call (with an optional folder override); the response then includes storageKey and url. There is no plugin-level storage toggle — passing saveToStorage: true without xStorage registered throws an actionable error. The plugin decorates Fastify with fastify.xPdf and registers itself as xpdf via fastify-plugin with a fastify: "5.x" constraint.
AI Context
package: "@xenterprises/fastify-xpdf"
type: fastify-plugin
use-when: PDF generation from HTML/Markdown/URL via Puppeteer; PDF manipulation (form fill, merge, extract pages, metadata) via pdf-lib
decorator: fastify.xPdf (generateFromHtml, generateFromMarkdown, generateFromUrl, fillForm, listFormFields, mergePDFs, extractPages, getPageCount, getMetadata)
env: none — plugin never reads process.env; all configuration via register options
helper-exports: "@xenterprises/fastify-xpdf/helpers" (generatePdfFilename, isValidPdfBuffer, sanitizeFilename, wrapHtmlTemplate, parseMargin, etc.)
optional-integration: fastify-xstorage — register first, then pass saveToStorage: true per call; throws if xStorage is missing
