PDF Lib
"pdf-lib: create and modify PDFs in JavaScript, form filling, page manipulation, embedding images/fonts, digital signatures"
pdf-lib operates at the PDF specification level, giving you direct control over the document structure without a rendering engine. It creates and modifies PDFs purely in JavaScript with zero native dependencies, making it the lightest option for PDF manipulation. Choose pdf-lib when you need to fill forms, merge documents, add watermarks, manipulate existing PDFs, or build documents programmatically without HTML. It runs identically in Node and the browser. The trade-off is that you handle all layout math yourself; there is no CSS or flexbox engine.
## Key Points
- Use `StandardFonts` when custom typography is not required; they are built into the PDF spec and add zero bytes to file size.
- Register fontkit only when embedding custom fonts; it is not needed for standard fonts.
- Call `form.flatten()` after filling forms to prevent end users from editing the filled values.
- Use `doc.copyPages()` to move pages between documents; never try to add a page object from one document directly to another.
- Set document metadata (`setTitle`, `setAuthor`, `setCreationDate`) for accessibility and organization.
- For large merge operations, process documents sequentially to keep memory usage predictable.
- Use `PDFDocument.load(bytes, { ignoreEncryption: true })` only when you have the legal right to process the encrypted document.
- Prefer `Uint8Array` over `Buffer` for cross-platform compatibility between Node and browser.
- **Manual text wrapping with hardcoded widths.** Use `font.widthOfTextAtSize()` to measure text and compute line breaks dynamically. Hardcoded positions break when content length varies.
- **Embedding the same font multiple times.** Embed once and reuse the `PDFFont` reference across all pages. Duplicate embedding bloats file size.
- **Loading entire large PDFs into memory for single-page extraction.** For very large files, consider streaming-capable libraries or processing page ranges.
- **Using pdf-lib for complex HTML layouts.** pdf-lib has no layout engine. If you need to render HTML or complex flowing text, use Puppeteer or React-PDF instead.skilldb get document-generation-services-skills/PDF LibFull skill: 373 linespdf-lib Document Generation
Core Philosophy
pdf-lib operates at the PDF specification level, giving you direct control over the document structure without a rendering engine. It creates and modifies PDFs purely in JavaScript with zero native dependencies, making it the lightest option for PDF manipulation. Choose pdf-lib when you need to fill forms, merge documents, add watermarks, manipulate existing PDFs, or build documents programmatically without HTML. It runs identically in Node and the browser. The trade-off is that you handle all layout math yourself; there is no CSS or flexbox engine.
Setup
// package.json dependencies
// "pdf-lib": "^1.17.1"
// "@pdf-lib/fontkit": "^1.1.1" (for custom font embedding)
import {
PDFDocument,
PDFPage,
PDFFont,
StandardFonts,
rgb,
degrees,
PageSizes,
PDFTextField,
PDFCheckBox,
} from "pdf-lib";
import fontkit from "@pdf-lib/fontkit";
import { readFile, writeFile } from "fs/promises";
// Helper: load a custom font into a document
async function embedCustomFont(
doc: PDFDocument,
fontPath: string
): Promise<PDFFont> {
doc.registerFontkit(fontkit);
const fontBytes = await readFile(fontPath);
return doc.embedFont(fontBytes);
}
Key Techniques
Creating a Document from Scratch
async function createInvoicePdf(data: InvoiceData): Promise<Uint8Array> {
const doc = await PDFDocument.create();
doc.setTitle(`Invoice ${data.number}`);
doc.setAuthor(data.companyName);
doc.setCreationDate(new Date());
const font = await doc.embedFont(StandardFonts.Helvetica);
const fontBold = await doc.embedFont(StandardFonts.HelveticaBold);
const page = doc.addPage(PageSizes.A4);
const { width, height } = page.getSize();
let y = height - 50;
// Header
page.drawText(`INVOICE #${data.number}`, {
x: 50,
y,
size: 24,
font: fontBold,
color: rgb(0.06, 0.09, 0.16),
});
y -= 30;
page.drawText(`Date: ${data.date}`, { x: 50, y, size: 11, font });
y -= 16;
page.drawText(`Customer: ${data.customer.name}`, { x: 50, y, size: 11, font });
y -= 30;
// Divider line
page.drawLine({
start: { x: 50, y },
end: { x: width - 50, y },
thickness: 1,
color: rgb(0.88, 0.91, 0.96),
});
y -= 20;
// Table header
const cols = { desc: 50, qty: 340, price: 410, total: 480 };
for (const [label, x] of Object.entries({
Description: cols.desc,
Qty: cols.qty,
Price: cols.price,
Total: cols.total,
})) {
page.drawText(label, { x, y, size: 10, font: fontBold });
}
y -= 18;
// Table rows
for (const item of data.items) {
page.drawText(item.description, { x: cols.desc, y, size: 10, font });
page.drawText(String(item.qty), { x: cols.qty, y, size: 10, font });
page.drawText(`$${item.price.toFixed(2)}`, { x: cols.price, y, size: 10, font });
page.drawText(`$${(item.qty * item.price).toFixed(2)}`, {
x: cols.total,
y,
size: 10,
font,
});
y -= 16;
}
// Total
y -= 10;
const total = data.items.reduce((s, i) => s + i.qty * i.price, 0);
page.drawText(`Total: $${total.toFixed(2)}`, {
x: cols.total - 30,
y,
size: 14,
font: fontBold,
});
return doc.save();
}
interface InvoiceData {
number: string;
date: string;
companyName: string;
customer: { name: string };
items: Array<{ description: string; qty: number; price: number }>;
}
Form Filling
async function fillTaxForm(
templatePath: string,
data: Record<string, string | boolean>
): Promise<Uint8Array> {
const templateBytes = await readFile(templatePath);
const doc = await PDFDocument.load(templateBytes);
const form = doc.getForm();
for (const [fieldName, value] of Object.entries(data)) {
if (typeof value === "boolean") {
const checkbox = form.getCheckBox(fieldName);
value ? checkbox.check() : checkbox.uncheck();
} else {
const textField = form.getTextField(fieldName);
textField.setText(value);
}
}
// Flatten form fields so they become static content
form.flatten();
return doc.save();
}
// Usage
const filled = await fillTaxForm("./templates/w9.pdf", {
name: "Acme Corporation",
business_name: "Acme Corp LLC",
address: "123 Main St",
city_state_zip: "San Francisco, CA 94102",
ein: "12-3456789",
is_llc: true,
});
await writeFile("./output/w9-filled.pdf", filled);
Merging PDFs
async function mergePdfs(pdfPaths: string[]): Promise<Uint8Array> {
const merged = await PDFDocument.create();
for (const path of pdfPaths) {
const bytes = await readFile(path);
const source = await PDFDocument.load(bytes);
const copiedPages = await merged.copyPages(
source,
source.getPageIndices()
);
for (const page of copiedPages) {
merged.addPage(page);
}
}
return merged.save();
}
// Merge with selective page extraction
async function extractPages(
sourcePath: string,
pageNumbers: number[]
): Promise<Uint8Array> {
const sourceBytes = await readFile(sourcePath);
const source = await PDFDocument.load(sourceBytes);
const output = await PDFDocument.create();
// pageNumbers are 1-based for the caller, convert to 0-based
const indices = pageNumbers.map((n) => n - 1);
const copiedPages = await output.copyPages(source, indices);
for (const page of copiedPages) {
output.addPage(page);
}
return output.save();
}
Adding Watermarks
async function addWatermark(
pdfBytes: Uint8Array,
watermarkText: string
): Promise<Uint8Array> {
const doc = await PDFDocument.load(pdfBytes);
const font = await doc.embedFont(StandardFonts.HelveticaBold);
const pages = doc.getPages();
for (const page of pages) {
const { width, height } = page.getSize();
const textWidth = font.widthOfTextAtSize(watermarkText, 60);
page.drawText(watermarkText, {
x: (width - textWidth) / 2,
y: height / 2,
size: 60,
font,
color: rgb(0.85, 0.85, 0.85),
rotate: degrees(-45),
opacity: 0.3,
});
}
return doc.save();
}
Embedding Images
async function addLetterhead(
pdfBytes: Uint8Array,
logoPath: string
): Promise<Uint8Array> {
const doc = await PDFDocument.load(pdfBytes);
const logoBytes = await readFile(logoPath);
// Detect format and embed accordingly
const logo = logoPath.endsWith(".png")
? await doc.embedPng(logoBytes)
: await doc.embedJpg(logoBytes);
const logoDims = logo.scale(0.25);
const pages = doc.getPages();
for (const page of pages) {
const { width } = page.getSize();
page.drawImage(logo, {
x: width - logoDims.width - 40,
y: page.getHeight() - logoDims.height - 20,
width: logoDims.width,
height: logoDims.height,
});
}
return doc.save();
}
Custom Fonts
async function createStyledDocument(): Promise<Uint8Array> {
const doc = await PDFDocument.create();
doc.registerFontkit(fontkit);
const interBytes = await readFile("./fonts/Inter-Regular.ttf");
const interBoldBytes = await readFile("./fonts/Inter-Bold.ttf");
const inter = await doc.embedFont(interBytes);
const interBold = await doc.embedFont(interBoldBytes);
const page = doc.addPage(PageSizes.A4);
let y = page.getHeight() - 60;
page.drawText("Custom Font Document", {
x: 50,
y,
size: 22,
font: interBold,
color: rgb(0.06, 0.09, 0.16),
});
y -= 30;
page.drawText("This body text uses the Inter typeface for clean rendering.", {
x: 50,
y,
size: 11,
font: inter,
color: rgb(0.1, 0.1, 0.1),
maxWidth: 500,
lineHeight: 16,
});
return doc.save();
}
Page Manipulation Utilities
async function rotatePagesLandscape(
pdfBytes: Uint8Array
): Promise<Uint8Array> {
const doc = await PDFDocument.load(pdfBytes);
for (const page of doc.getPages()) {
const currentRotation = page.getRotation().angle;
page.setRotation(degrees(currentRotation + 90));
}
return doc.save();
}
async function addPageNumbers(pdfBytes: Uint8Array): Promise<Uint8Array> {
const doc = await PDFDocument.load(pdfBytes);
const font = await doc.embedFont(StandardFonts.Helvetica);
const pages = doc.getPages();
const total = pages.length;
pages.forEach((page, index) => {
const { width } = page.getSize();
const text = `${index + 1} / ${total}`;
const textWidth = font.widthOfTextAtSize(text, 9);
page.drawText(text, {
x: (width - textWidth) / 2,
y: 20,
size: 9,
font,
color: rgb(0.6, 0.6, 0.6),
});
});
return doc.save();
}
Best Practices
- Use
StandardFontswhen custom typography is not required; they are built into the PDF spec and add zero bytes to file size. - Register fontkit only when embedding custom fonts; it is not needed for standard fonts.
- Call
form.flatten()after filling forms to prevent end users from editing the filled values. - Use
doc.copyPages()to move pages between documents; never try to add a page object from one document directly to another. - Set document metadata (
setTitle,setAuthor,setCreationDate) for accessibility and organization. - For large merge operations, process documents sequentially to keep memory usage predictable.
- Use
PDFDocument.load(bytes, { ignoreEncryption: true })only when you have the legal right to process the encrypted document. - Prefer
Uint8ArrayoverBufferfor cross-platform compatibility between Node and browser.
Anti-Patterns
- Manual text wrapping with hardcoded widths. Use
font.widthOfTextAtSize()to measure text and compute line breaks dynamically. Hardcoded positions break when content length varies. - Embedding the same font multiple times. Embed once and reuse the
PDFFontreference across all pages. Duplicate embedding bloats file size. - Modifying page content without understanding coordinate origin. PDF coordinates start at the bottom-left, not top-left. Placing elements with Y values counting from the top requires subtracting from
page.getHeight(). - Loading entire large PDFs into memory for single-page extraction. For very large files, consider streaming-capable libraries or processing page ranges.
- Using pdf-lib for complex HTML layouts. pdf-lib has no layout engine. If you need to render HTML or complex flowing text, use Puppeteer or React-PDF instead.
- Forgetting to call
doc.save(). All modifications are in memory untilsave()serializes them. Returning the original bytes instead of the save result is a common bug.
Install this skill directly: skilldb add document-generation-services-skills
Related Skills
Docraptor
"DocRaptor: HTML-to-PDF API, Prince XML engine, CSS print styles, headers/footers, page breaks, async documents"
Docusaurus
Docusaurus: React-based static site generator for documentation sites, versioned docs, MDX support, search integration, i18n
Jspdf
jsPDF: client-side and server-side PDF generation in JavaScript, tables, images, custom fonts, autotable plugin
Latex Node
LaTeX with Node.js: compile LaTeX documents programmatically, template-based PDF generation, mathematical typesetting, academic papers
Markdoc
Markdoc: Stripe's Markdown-based authoring framework for structured documentation, custom tags, validation, and renderers
Puppeteer
"Puppeteer: headless Chrome, PDF generation from HTML, screenshots, web scraping, page automation, Chromium control"