X Enterprises

fastify-xpdf

Fastify 5 plugin for PDF generation and manipulation — HTML/Markdown/URL to PDF via Puppeteer, form filling, merging, page extraction, and metadata via pdf-lib, with optional S3 storage.

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.xPDF to fastify.xPdf (lowercase df). Update every call site. The plugin-level useStorage option was also removed — saving to storage is now gated per call via saveToStorage: 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

NameTypeDefaultRequiredDescription
headlessbooleantrueNoRun Puppeteer in headless mode
argsstring[]["--no-sandbox", "--disable-setuid-sandbox"]NoChrome launch arguments
browserFactoryfunctionpuppeteer.launchNoInjectable browser factory (launchOptions) => Promise<browser>. Receives { headless, args }; used by tests to substitute a fake browser without launching Chrome
defaultFolderstring"pdfs"NoDefault storage folder for saved PDFs
formatstring"A4"NoDefault page format (A4, Letter, A3, A5, Tabloid, etc.)
printBackgroundbooleantrueNoPrint background graphics and colours
marginobject{ top: "1cm", right: "1cm", bottom: "1cm", left: "1cm" }NoDefault page margins (CSS units)

Methods

All methods are on fastify.xPdf.

Generation (Puppeteer-based):

Form operations (pdf-lib):

Manipulation (pdf-lib):

Exported Helpers

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

HelperDescription
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:

ErrorCause
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]:

ErrorCause
[xPDF] HTML content must be a non-empty stringEmpty/null HTML in generateFromHtml
[xPDF] Markdown content must be a non-empty stringEmpty/null markdown
[xPDF] URL must be a non-empty stringEmpty/null URL
[xPDF] URL must be a valid URLMalformed URL string
[xPDF] Invalid PDF bufferNon-PDF buffer passed to manipulation methods
[xPDF] fieldValues must be an objectNon-object fieldValues in fillForm
[xPDF] pdfBuffers must be a non-empty array...Empty/null array in mergePDFs
[xPDF] One or more invalid PDF buffers providedInvalid 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 integerNon-integer or negative index
[xPDF] Page index N out of range...Index exceeds PDF page count
[xPDF] Failed to initialize PDF browserPuppeteer browser launch failure
[xPDF] Failed to process PDF during mergeCorrupt PDF encountered during merge
xpdf: saveToStorage requires the xStorage plugin — register @xenterprises/fastify-xstorage first or pass saveToStorage: falsesaveToStorage: 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
Copyright © 2026