@xenterprises/fastify-xpdf
v1.0.1
Published
Fastify plugin for PDF generation and manipulation. Convert HTML/Markdown to PDF, fill forms, and merge PDFs.
Readme
xPDF
Fastify v5 plugin for PDF generation and manipulation with a simple, intuitive library of methods.
Generate PDFs from HTML and Markdown, fill PDF forms, and merge PDFs. Works with any Fastify application and optionally integrates with xStorage for automatic cloud storage.
Requirements
- Fastify v5.0.0+
- Node.js v20+
Features
- 📄 HTML to PDF - Convert HTML to PDF using Puppeteer
- 📝 Markdown to PDF - Convert Markdown to PDF with styled formatting
- 📋 Form Filling - Fill PDF form fields with values
- ✏️ Form Flattening - Make form fields non-editable
- 🔗 PDF Merging - Combine multiple PDFs into one
- 💾 Optional Storage - Save PDFs to S3-compatible storage via xStorage
- 🔌 Simple API - Methods decorated on Fastify instance
- 🎯 Library Pattern - Use as a service, not as HTTP routes
Installation
npm install @xenterprises/fastify-xpdf puppeteer marked pdf-lib fastify@5For optional storage integration:
npm install @xenterprises/fastify-xstorageQuick Start
import Fastify from "fastify";
import xPDF from "@xenterprises/fastify-xpdf";
const fastify = Fastify({ logger: true });
// Register xPDF
await fastify.register(xPDF, {
headless: true,
useStorage: false,
defaultFolder: "pdfs",
});
// Generate PDF from HTML
const result = await fastify.xPDF.generateFromHtml("<h1>Hello World</h1>", {
filename: "hello.pdf",
});
console.log(result);
// {
// buffer: Buffer,
// filename: "hello.pdf",
// size: 12345,
// storageKey?: "pdfs/hello.pdf", // Only if saveToStorage: true
// url?: "https://..." // Only if saveToStorage: true
// }
await fastify.listen({ port: 3000 });Configuration
Plugin Options
await fastify.register(xPDF, {
// Puppeteer options
headless: true, // Default: true
args: ["--no-sandbox"], // Chrome launch args
// xStorage integration (optional)
useStorage: false, // Default: false
defaultFolder: "pdfs", // Default storage folder
// PDF generation defaults
format: "A4", // Default page format
printBackground: true, // Include background in PDFs
margin: { // Default margins
top: "1cm",
right: "1cm",
bottom: "1cm",
left: "1cm",
},
});Core API
All methods are available on the fastify.xPDF namespace.
fastify.xPDF.generateFromHtml(html, options)
Convert HTML to PDF.
const result = await fastify.xPDF.generateFromHtml("<h1>Invoice</h1>", {
filename: "invoice.pdf",
format: "A4",
landscape: false,
margin: { top: "1cm", right: "1cm", bottom: "1cm", left: "1cm" },
printBackground: true,
saveToStorage: false,
folder: "pdfs",
});
console.log(result);
// {
// buffer: Buffer, // PDF content as Buffer
// filename: "invoice.pdf", // Output filename
// size: 245678, // File size in bytes
// storageKey?: "pdfs/...", // Only if saveToStorage: true
// url?: "https://..." // Only if saveToStorage: true
// }Options:
filename- Output filename (default:generated-{timestamp}.pdf)format- Page format: A4, Letter, Legal, A3, A5, etc. (default: A4)landscape- Landscape orientation (default: false)margin- Page margins (default: 1cm all sides)printBackground- Include background graphics (default: true)saveToStorage- Save to xStorage (default: false)folder- Storage folder (default: 'pdfs')
fastify.xPDF.generateFromMarkdown(markdown, options)
Convert Markdown to PDF.
const markdown = `
# Invoice
**Date:** January 1, 2024
## Items
1. Item 1: $100
2. Item 2: $200
**Total:** $300
`;
const result = await fastify.xPDF.generateFromMarkdown(markdown, {
filename: "invoice.pdf",
saveToStorage: true,
folder: "invoices",
});
console.log(result);
// {
// buffer: Buffer,
// filename: "invoice.pdf",
// size: 234567,
// storageKey: "invoices/invoice-1234567890.pdf",
// url: "https://storage.example.com/invoices/invoice-1234567890.pdf"
// }Supports:
- Headings (H1-H6)
- Paragraphs and line breaks
- Lists (ordered and unordered)
- Code blocks with syntax highlighting
- Tables
- Blockquotes
- Links
- Emphasis (bold, italic)
fastify.xPDF.fillForm(pdfBuffer, fieldValues, options)
Fill PDF form fields and optionally flatten.
// Download PDF from storage
const pdfBuffer = await fastify.xStorage.download("forms/application.pdf");
// Fill form
const result = await fastify.xPDF.fillForm(
pdfBuffer,
{
firstName: "John",
lastName: "Doe",
email: "[email protected]",
agreeToTerms: true,
},
{
flatten: true, // Make fields non-editable
filename: "application-filled.pdf",
saveToStorage: true,
folder: "submitted-forms",
}
);
console.log(result);
// {
// buffer: Buffer,
// filename: "application-filled.pdf",
// size: 256789,
// storageKey: "submitted-forms/...",
// url: "https://..."
// }Options:
flatten- Make form fields non-editable (default: true)filename- Output filenamesaveToStorage- Save to xStorage (default: false)folder- Storage folder (default: 'pdfs')
Supported Field Types:
- Text fields:
field.setText(value) - Checkboxes:
field.check()orfield.uncheck() - Radio buttons:
field.select(value) - Dropdowns:
field.select(value)
fastify.xPDF.listFormFields(pdfBuffer)
List all form fields in a PDF.
const pdfBuffer = await fastify.xStorage.download("forms/application.pdf");
const fields = await fastify.xPDF.listFormFields(pdfBuffer);
console.log(fields);
// [
// { name: "firstName", type: "text", value: null },
// { name: "lastName", type: "text", value: null },
// { name: "email", type: "text", value: null },
// { name: "agreeToTerms", type: "checkbox", value: false }
// ]Returns:
name- Field nametype- Field type (text, checkbox, radio, dropdown, option)value- Current field value
fastify.xPDF.mergePDFs(pdfBuffers, options)
Merge multiple PDFs into one.
// Download PDFs from storage
const page1 = await fastify.xStorage.download("documents/page1.pdf");
const page2 = await fastify.xStorage.download("documents/page2.pdf");
const page3 = await fastify.xStorage.download("documents/page3.pdf");
const result = await fastify.xPDF.mergePDFs([page1, page2, page3], {
filename: "complete-document.pdf",
saveToStorage: true,
folder: "merged",
});
console.log(result);
// {
// buffer: Buffer,
// filename: "complete-document.pdf",
// size: 512345,
// pageCount: 15, // Total pages from all 3 PDFs
// storageKey: "merged/complete-document-1234567890.pdf",
// url: "https://..."
// }Options:
filename- Output filename (default:merged-{timestamp}.pdf)saveToStorage- Save to xStorage (default: false)folder- Storage folder (default: 'pdfs')
Returns:
buffer- Merged PDF bufferfilename- Output filenamesize- File size in bytespageCount- Total number of pagesstorageKey- Storage key (if saved)url- Public URL (if saved)
xStorage Integration
xPDF works seamlessly with xStorage for automatic cloud storage of generated PDFs.
Basic Integration
import Fastify from "fastify";
import xStorage from "@xenterprises/fastify-xstorage";
import xPDF from "@xenterprises/fastify-xpdf";
const fastify = Fastify();
// Register xStorage first
await fastify.register(xStorage, {
endpoint: "https://nyc3.digitaloceanspaces.com",
region: "nyc3",
accessKeyId: process.env.DO_SPACES_KEY,
secretAccessKey: process.env.DO_SPACES_SECRET,
bucket: "my-bucket",
publicUrl: "https://my-bucket.nyc3.digitaloceanspaces.com",
});
// Register xPDF (will auto-detect xStorage)
await fastify.register(xPDF, {
useStorage: true,
defaultFolder: "pdfs",
});
// Now all PDFs can be saved to storage
const result = await fastify.xPDF.generateFromHtml("<h1>Report</h1>", {
saveToStorage: true,
folder: "reports",
});
console.log(result.url); // https://my-bucket.nyc3.digitaloceanspaces.com/reports/...Advanced: Retrieve and Process Stored PDFs
// Get PDF from storage
const storedUrl = "https://my-bucket.nyc3.digitaloceanspaces.com/forms/template.pdf";
const key = storedUrl.replace(process.env.STORAGE_PUBLIC_URL + "/", "");
const pdfBuffer = await fastify.xStorage.download(key);
// Fill form and save result
const result = await fastify.xPDF.fillForm(pdfBuffer, { name: "John" }, {
saveToStorage: true,
folder: "submitted",
});
console.log(result.url); // Direct link to filled formHelper Utilities
import { helpers } from "@xenterprises/fastify-xpdf";
// Generate unique PDF filename
helpers.generatePdfFilename("invoice");
// "invoice-1704067200000.pdf"
// Validate PDF buffer
helpers.isValidPdfBuffer(buffer);
// true/false
// Get PDF metadata
helpers.getPdfMetadata(buffer);
// { size: 12345 }
// Sanitize filename
helpers.sanitizeFilename("My File (2024).pdf");
// "my_file_2024.pdf"
// Get page format dimensions
helpers.getPageFormat("A4");
// { width: 8.27, height: 11.7 }Usage Examples
Document Generation
fastify.post("/invoices", async (request, reply) => {
const { items, total, date } = request.body;
const html = `
<h1>Invoice</h1>
<p>Date: ${date}</p>
<ul>
${items.map((item) => `<li>${item.name}: $${item.price}</li>`).join("")}
</ul>
<h2>Total: $${total}</h2>
`;
const result = await fastify.xPDF.generateFromHtml(html, {
filename: "invoice.pdf",
saveToStorage: true,
folder: "invoices",
});
return { success: true, url: result.url };
});Form Processing
fastify.post("/applications/:id/submit", async (request, reply) => {
const { id } = request.params;
const formData = request.body;
// Get form template
const template = await fastify.xStorage.download("forms/application-template.pdf");
// Fill with submitted data
const result = await fastify.xPDF.fillForm(template, formData, {
flatten: true,
filename: `application-${id}.pdf`,
saveToStorage: true,
folder: "applications",
});
// Save record in database
await fastify.db.application.create({
id,
pdfKey: result.storageKey,
pdfUrl: result.url,
submittedAt: new Date(),
});
return { success: true, pdfUrl: result.url };
});Report Compilation
fastify.post("/reports/compile", async (request, reply) => {
const { reportIds } = request.body;
// Get all report PDFs
const pdfBuffers = await Promise.all(
reportIds.map((id) =>
fastify.xStorage.download(`reports/${id}.pdf`)
)
);
// Merge into single document
const result = await fastify.xPDF.mergePDFs(pdfBuffers, {
filename: "compiled-report.pdf",
saveToStorage: true,
folder: "compiled",
});
return {
success: true,
filename: result.filename,
pages: result.pageCount,
url: result.url,
};
});Best Practices
- Use xStorage for Production - Automatically manage PDF storage and access
- Validate Input - Sanitize HTML/Markdown to prevent XSS in PDF generation
- Handle Large PDFs - Set reasonable timeouts for Puppeteer operations
- Organize with Folders - Use meaningful folder structures (e.g., "invoices/2024")
- Store Metadata - Keep records of storage keys in your database
- Error Handling - Always wrap PDF operations in try-catch blocks
- Memory Management - PDFs are loaded in memory; handle size carefully
- Reuse Browser - Plugin maintains single browser instance for efficiency
Configuration Profiles
Development
await fastify.register(xPDF, {
headless: true,
useStorage: false, // Save to disk instead
format: "A4",
});Production with Storage
await fastify.register(xPDF, {
headless: true,
useStorage: true,
defaultFolder: "pdfs",
args: ["--no-sandbox", "--disable-setuid-sandbox"],
});High Performance
await fastify.register(xPDF, {
headless: true,
args: [
"--no-sandbox",
"--disable-setuid-sandbox",
"--disable-gpu",
"--disable-dev-shm-usage",
],
useStorage: true,
});Troubleshooting
Puppeteer Launch Fails
Error: Timeout waiting for browser to start
Solution: Increase available memory and ensure Chrome dependencies are installed
# macOS
brew install chromium
# Linux
apt-get install -y chromium-browser
# Or use args to help with sandboxing
headless: true,
args: ["--no-sandbox", "--disable-setuid-sandbox", "--disable-gpu"]Form Fields Not Filling
Error: Form field not found: fieldName
Solution: List form fields first to verify correct names
const fields = await fastify.xPDF.listFormFields(pdfBuffer);
console.log(fields); // Check exact field namesPDF Generation Times Out
Error: Timeout waiting for page to load
Solution: Simplify HTML, remove external resources, or optimize content
// ❌ Avoid external resources
const html = '<img src="https://example.com/image.jpg">';
// ✅ Use inline or relative paths
const html = '<img src="/images/logo.png">';Storage Integration Not Working
Error: xStorage plugin not registered
Solution: Register xStorage before xPDF
// Register xStorage FIRST
await fastify.register(xStorage, {...});
// Then register xPDF
await fastify.register(xPDF, {
useStorage: true,
});Performance Tips
- Use
headless: true(already default) - Enable
printBackground: falsefor simpler documents - Avoid large images or external resources in HTML
- Use Markdown for simple, styled documents
- Batch operations when merging many PDFs
Testing
npm testSee TESTING.md for comprehensive testing guide.
Examples
See EXAMPLES.md for complete real-world examples.
License
ISC
