Skip to main content

Generate PDF Using PDF-lib in Node.js

· 21 min read
Michał Szymanowski
Michał Szymanowski
PDFBolt Co-Founder

Visual representation of PDF generation with PDF-lib in Node.js

For developers building Node.js applications, the ability to programmatically generate and modify PDFs is a powerful capability that opens up numerous possibilities for document automation. PDF‑lib is a pure JavaScript library for creating and modifying PDF documents. Unlike many alternatives, PDF-lib doesn't rely on native dependencies, making it lightweight and easy to integrate into any Node.js project. This guide will walk you through everything you need to know about generating PDFs using PDF-lib in Node.js applications.

How PDF-lib Works

At its core, PDF-lib is designed to manipulate PDF documents without external dependencies.

Here's how it works:

  • Direct PDF Manipulation: PDF-lib works directly with the PDF format itself – it does not convert HTML to PDF. This is an important distinction, as PDF-lib is designed for programmatic creation and modification of PDF documents, not for rendering web content into PDFs.

  • PDF Document Model: PDF-lib creates an in-memory model of a PDF document that mirrors the actual PDF structure. This model includes pages, content streams, objects, and metadata.

  • Document Creation Flow: When you create a PDF with PDF-lib, you typically:

    • Create a document object – PDFDocument.
    • Add pages to the document.
    • Embed fonts and other resources (images, etc.).
    • Draw content (text, shapes, images) on pages.
    • Save the document to bytes which can be written to a file.
  • Content Coordinates: PDF-lib uses a coordinate system where (0,0) is at the bottom-left corner of the page, with positive x-axis extending to the right and positive y-axis extending upward.

  • Asynchronous Operations: Many PDF-lib operations return Promises, allowing for efficient handling of resource-intensive operations.

Why Choose PDF-lib?

Before diving into implementation, let's understand why PDF-lib stands out:

  • Pure JavaScript: No native dependencies or binaries required.
  • TypeScript compatibility: Includes TypeScript type definitions.
  • Small footprint: Lightweight compared to many alternatives.
  • Active development: Regular updates and good community support.
  • Comprehensive API: Supports text, images, forms, digital signatures, and more.

Installation

Let's start by adding PDF-lib to your Node.js project:

npm install pdf-lib

For TypeScript users, the types are included, so no additional packages are needed.

Basic PDF Generation with PDF-lib

Creating a Simple PDF Document

Here's a basic example showing how to create a PDF with text:

const { PDFDocument, rgb, StandardFonts } = require('pdf-lib');
const fs = require('fs').promises;

async function createPdf() {
// Create a new empty PDF document
const pdfDoc = await PDFDocument.create();

// Add a new page to the document with default A4 size
const page = pdfDoc.addPage();

// Embed Helvetica font (built-in standard font)
const helveticaFont = await pdfDoc.embedFont(StandardFonts.Helvetica);

// Get page dimensions for positioning
const { width, height } = page.getSize();
const fontSize = 30;

// Place text on the page with specific coordinates
page.drawText('Hello!', {
x: 50, // Horizontal position from left edge
y: height - 100, // Vertical position from bottom edge
size: fontSize,
font: helveticaFont,
color: rgb(0, 0, 0),
});

page.drawText('Hope you have an awesome day!', {
x: 50,
y: height - 180,
size: fontSize,
font: helveticaFont,
color: rgb(0, 0, 1),
});

// Convert document to file and save
const pdfBytes = await pdfDoc.save();
await fs.writeFile('output.pdf', pdfBytes);

console.log('PDF created successfully! (output.pdf)');
}

// Handle any potential errors
createPdf().catch(err => {
console.error('Error creating PDF:', err);
});

This basic script creates a PDF with a single page and adds text to it:

Simple PDF document with some text

Adding Images to Your PDF

You can also embed images in your PDF documents:

const { PDFDocument } = require('pdf-lib');
const fs = require('fs').promises;

async function createPdfWithImage() {
// Create a new empty PDF document
const pdfDoc = await PDFDocument.create();

// Add a new page to the document
const page = pdfDoc.addPage();

// Read local image file
const imageData = await fs.readFile('image.jpg');

// Embed image into the PDF
const embeddedImage = await pdfDoc.embedJpg(imageData);

// Scale image to fit the page
const imageDimensions = embeddedImage.scale(0.4);

// Get page dimensions for positioning
const { width, height } = page.getSize();

// Draw image centered horizontally
page.drawImage(embeddedImage, {
x: (width - imageDimensions.width) / 2,
y: height - imageDimensions.height - 50,
width: imageDimensions.width,
height: imageDimensions.height,
});

// Convert document to file and save
const pdfBytes = await pdfDoc.save();
await fs.writeFile('output-with-image.pdf', pdfBytes);

console.log('PDF with image created successfully!');
}

// Handle any potential errors
createPdfWithImage().catch(err => {
console.error('Error creating PDF with image:', err);
});
PDF-lib supports JPEG and PNG formats
  • For PNG images – use pdfDoc.embedPng().
  • For JPEG – use embedJpg().

PDF document showing an embedded image

Modifying Existing PDFs

One of PDF-lib's strengths is its ability to modify existing PDF documents.

Adding a New Page

In the example below, we load an existing PDF, add a new page to it, and place example text:

const { PDFDocument, StandardFonts, rgb } = require('pdf-lib');
const fs = require('fs').promises;

async function modifyPdf() {
// Load existing PDF
const existingPdfData = await fs.readFile('example.pdf');
const pdfDoc = await PDFDocument.load(existingPdfData);

// Add a new page
const page = pdfDoc.addPage();

// Embed the standard Helvetica font
const helveticaFont = await pdfDoc.embedFont(StandardFonts.Helvetica);

// Get page dimensions
const { width, height } = page.getSize();

// Draw text on the new page
page.drawText('This page was added using PDF-lib!', {
x: 50,
y: height - 100,
size: 30,
font: helveticaFont,
color: rgb(0, 0.53, 0.71),
});

// Save the modified PDF
const modifiedPdfBytes = await pdfDoc.save();
await fs.writeFile('modified.pdf', modifiedPdfBytes);

console.log('PDF modified successfully!');
}

// Handle any potential errors
modifyPdf().catch(err => {
console.error('Error modifying PDF:', err);
});

Here is an example output:

Modified PDF with newly added page

Adding a Watermark to Each Page

In this next example, we load an existing PDF and add a watermark to each page:

const { PDFDocument, StandardFonts, rgb, degrees } = require('pdf-lib');
const fs = require('fs').promises;

async function addWatermark() {
// Load existing PDF
const existingPdfData = await fs.readFile('file-sample.pdf');
const pdfDoc = await PDFDocument.load(existingPdfData);

// Embed the standard Helvetica font
const helveticaFont = await pdfDoc.embedFont(StandardFonts.Helvetica);

// Get all pages in the document
const pages = pdfDoc.getPages();

// Add watermark to each page
pages.forEach(page => {
const { width, height } = page.getSize();

const watermarkText = 'CONFIDENTIAL';
const fontSize = 75;

page.drawText(watermarkText, {
x: width / 2 - 180,
y: height / 2 - 200,
size: fontSize,
font: helveticaFont,
color: rgb(0.9, 0.1, 0.1),
opacity: 0.2,
rotate: degrees(45),
});
});

// Save the modified PDF
const modifiedPdfBytes = await pdfDoc.save();
await fs.writeFile('watermarked.pdf', modifiedPdfBytes);

console.log('PDF watermarked successfully!');
}

// Handle any potential errors
addWatermark().catch(err => {
console.error('Error adding watermark:', err);
});

Example output:

PDF with diagonal watermark across the page

Merging Multiple PDFs

Combining multiple PDF documents into one:

const { PDFDocument } = require('pdf-lib');
const fs = require('fs').promises;

async function mergePdfs() {
try {
// Create a new PDF document
const mergedPdf = await PDFDocument.create();

// Load the first PDF
const pdf1Bytes = await fs.readFile('document1.pdf');
const pdf1 = await PDFDocument.load(pdf1Bytes);

// Load the second PDF
const pdf2Bytes = await fs.readFile('document2.pdf');
const pdf2 = await PDFDocument.load(pdf2Bytes);

// Copy pages from the first document
const pdf1Pages = await mergedPdf.copyPages(pdf1, pdf1.getPageIndices());
for (const page of pdf1Pages) {
mergedPdf.addPage(page);
}

// Copy pages from the second document
const pdf2Pages = await mergedPdf.copyPages(pdf2, pdf2.getPageIndices());
for (const page of pdf2Pages) {
mergedPdf.addPage(page);
}

// Save the merged PDF
const mergedPdfBytes = await mergedPdf.save();
await fs.writeFile('merged.pdf', mergedPdfBytes);

console.log('PDFs merged successfully!');
} catch (err) {
console.error('Error merging PDFs:', err);
if (err.code === 'ENOENT') {
console.error('One of the PDF files does not exist');
}
}
}

mergePdfs();

Creating Forms

PDF-lib allows you to create interactive forms in PDF documents, such as text inputs, radio buttons, checkboxes, dropdowns, and option lists.

Below is an example of how to create such a form:

Click to view the example
const { PDFDocument } = require('pdf-lib');
const fs = require('fs').promises;

async function createForm() {
// Create a new PDF document
const pdfDoc = await PDFDocument.create();

// Add a new page with specified dimensions
const page = pdfDoc.addPage();

// Create a form for the document
const form = pdfDoc.getForm();

// Add text input field
page.drawText('Enter your most ridiculous superpower:', { x: 50, y: 800, size: 16 });
const superpowerInput = form.createTextField('superpower.input');
superpowerInput.setText('Professional Nap Taking');
superpowerInput.addToPage(page, { x: 50, y: 760, width: 440, height: 25 });

// Add radio button group
page.drawText('Choose your ultimate procrastination method:', { x: 50, y: 700, size: 16 });
page.drawText('Binge-watching cat videos', { x: 80, y: 670, size: 14 });
page.drawText('Organizing sock drawer', { x: 80, y: 640, size: 14 });
page.drawText('Scrolling memes', { x: 330, y: 670, size: 14 });
page.drawText('Inventing excuses', { x: 330, y: 640, size: 14 });

const procrastinationGroup = form.createRadioGroup('procrastination.method');
procrastinationGroup.addOptionToPage('Binge-watching cat videos', page, { x: 50, y: 670, width: 15, height: 15 });
procrastinationGroup.addOptionToPage('Organizing sock drawer', page, { x: 50, y: 640, width: 15, height: 15 });
procrastinationGroup.addOptionToPage('Scrolling memes', page, { x: 300, y: 670, width: 15, height: 15 });
procrastinationGroup.addOptionToPage('Inventing excuses', page, { x: 300, y: 640, width: 15, height: 15 });
procrastinationGroup.select('Scrolling memes');

// Add checkboxes
page.drawText('Select your dream vacation activities:', { x: 50, y: 580, size: 16 });
page.drawText('Beach lounging', { x: 80, y: 540, size: 14 });
page.drawText('Adventure sports', { x: 80, y: 510, size: 14 });
page.drawText('Cultural exploration', { x: 330, y: 540, size: 14 });
page.drawText('Foodie adventures', { x: 330, y: 510, size: 14 });

const firstActivityBox = form.createCheckBox('vacation.activity1');
const secondActivityBox = form.createCheckBox('vacation.activity2');
const thirdActivityBox = form.createCheckBox('vacation.activity3');
const fourthActivityBox = form.createCheckBox('vacation.activity4');

firstActivityBox.addToPage(page, { x: 50, y: 540, width: 15, height: 15 });
secondActivityBox.addToPage(page, { x: 50, y: 510, width: 15, height: 15 });
thirdActivityBox.addToPage(page, { x: 300, y: 540, width: 15, height: 15 });
fourthActivityBox.addToPage(page, { x: 300, y: 510, width: 15, height: 15 });

// Pre-check some activities
firstActivityBox.check();
fourthActivityBox.check();

// Add dropdown for snack selection
page.drawText('Choose your snack:', { x: 50, y: 440, size: 16 });
const snackDropdown = form.createDropdown('snack.selection');
snackDropdown.addOptions(['Pizza', 'Chocolate', 'Nachos', 'Ice Cream']);
snackDropdown.select('Nachos');
snackDropdown.addToPage(page, { x: 50, y: 400, width: 440, height: 25 });

// Add option list
page.drawText('Select your ultimate awkward moment:', { x: 50, y: 340, size: 16 });
const awkwardMomentList = form.createOptionList('awkward.moment');
awkwardMomentList.addOptions([
'Waving back at no one',
'Laughing alone in public',
'Talking to yourself',
'Tripping on flat ground',
'Misheard conversation'
]);
awkwardMomentList.select('Tripping on flat ground');
awkwardMomentList.addToPage(page, { x: 50, y: 220, width: 440, height: 100 });

// Save the PDF document
const pdfBytes = await pdfDoc.save();
await fs.writeFile('form.pdf', pdfBytes);
console.log('Hilarious form created successfully!');
}

// Run the function and handle any errors
createForm().catch(console.error);

Here is the preview of output:

Interactive PDF form with text inputs, radio buttons, checkboxes, and dropdown menus

Click to view the interactive PDF

Adding Metadata to Your PDF

Metadata helps organize and describe your document. It includes properties like title, author, keywords, and more, which are useful for searchability and categorization.

Here's how to add metadata to a PDF:

const { PDFDocument, rgb, StandardFonts } = require('pdf-lib');
const fs = require('fs').promises;

async function createPdf() {
// Create a new PDF document
const pdfDoc = await PDFDocument.create();

// Add a new page
const page = pdfDoc.addPage();

// Embed Helvetica font
const helveticaFont = await pdfDoc.embedFont(StandardFonts.Helvetica);

// Place some text on the page
page.drawText('This document includes metadata information.', {
x: 50,
y: 750,
size: 24,
font: helveticaFont,
color: rgb(0, 0.5, 0),
});

// Set document metadata
pdfDoc.setTitle('The Coding Adventure');
pdfDoc.setAuthor('Curious Developer');
pdfDoc.setSubject('Exploring PDF Generation');
pdfDoc.setKeywords(['pdf-lib', 'nodejs', 'metadata']);
pdfDoc.setCreator('Code Craft Workshop');
pdfDoc.setProducer('Learning Project');

// Save the PDF
const pdfBytes = await pdfDoc.save();
await fs.writeFile('metadata.pdf', pdfBytes);

console.log('PDF with metadata created successfully!');
}

// Handle any potential errors
createPdf().catch(err => {
console.error('Error creating PDF:', err);
});

Drawing Shapes and Lines

PDF-lib also allows you to draw various shapes:

const { PDFDocument, rgb, StandardFonts } = require('pdf-lib');
const fs = require('fs').promises;

async function createPdfWithShapes() {
// Create a new PDF document
const pdfDoc = await PDFDocument.create();

// Add a blank page
const page = pdfDoc.addPage();

// Draw a rectangle
page.drawRectangle({
x: 50,
y: 700,
width: 100,
height: 100,
color: rgb(1, 0, 0), // Red fill
borderColor: rgb(0, 0, 0), // Black border
borderWidth: 2,
opacity: 0.5, // Semi-transparent
});

// Draw a circle
const centerX = 300;
const centerY = 700;
const radius = 50;

page.drawCircle({
x: centerX,
y: centerY,
radius: radius,
color: rgb(0, 0, 1), // Blue fill
borderColor: rgb(0, 0, 0), // Black border
borderWidth: 2,
});

// Draw lines
page.drawLine({
start: { x: 50, y: 550 },
end: { x: 500, y: 550 },
thickness: 3,
color: rgb(0, 0, 0),
dashArray: [10, 5], // Dashed line
});

// Draw a solid line
page.drawLine({
start: { x: 50, y: 500 },
end: { x: 500, y: 500 },
thickness: 3,
color: rgb(0, 0.7, 0.5)
});

// Save the PDF
const pdfBytes = await pdfDoc.save();
await fs.writeFile('shapes.pdf', pdfBytes);

console.log('PDF with shapes created successfully!');
}

// Handle any potential errors
createPdfWithShapes().catch(err => {
console.error('Error creating PDF:', err);
});

Here is the output:

PDF showing various shapes including rectangle, circle, and lines with different styling

Handling Text Wrapping and Line Breaks

PDF-lib offers simple text wrapping for creating professional-looking documents. This example shows how to automatically format text within a specified width, ensuring clean and readable PDFs.

The code demonstrates creating a PDF with dynamically wrapped text using PDF-lib's drawText:

const { PDFDocument, StandardFonts, rgb } = require('pdf-lib');
const fs = require('fs').promises;

async function createPdfWithWrappedText() {
try {
// Create a new PDF document
const pdfDoc = await PDFDocument.create();

// Add a new page
const page = pdfDoc.addPage();

// Get page dimensions
const { width, height } = page.getSize();

// Embed a standard font
const font = await pdfDoc.embedFont(StandardFonts.TimesRoman);

// Sample text to be wrapped
const text = 'This is a long paragraph that will be automatically wrapped to ' +
'fit within the specified width. PDF-lib provides options for controlling ' +
'the line height and text alignment. This makes it easier to create ' +
'documents with well-formatted text that remains readable.';

// Draw text
page.drawText(text, {
x: 50,
y: height - 50,
size: 14,
font: font,
color: rgb(0, 0, 0),
maxWidth: width - 100,
lineHeight: 16
});

// Save the PDF
const pdfBytes = await pdfDoc.save();
await fs.writeFile('wrapped-text.pdf', pdfBytes);

console.log('PDF with wrapped text created successfully!');
} catch (err) {
console.error('Error creating PDF with wrapped text:', err);
}
}

createPdfWithWrappedText();

Here is the output:

PDF showing text paragraph with proper line wrapping


info

For more advanced use cases, explore the official PDF-lib documentation and its extensive set of examples.

Real-World Example: Creating a Professional Invoice

Let's put everything together by creating a practical example: a professional-looking invoice. This example demonstrates how to combine text, tables, images, and styling to create a document that looks great and serves a real business purpose.

Click to view complete code
const {PDFDocument, rgb, StandardFonts, degrees} = require('pdf-lib');
const fs = require('fs').promises;

// Invoice data
const invoiceData = {
// Company information
company: {
name: 'AWESOME COMPANY',
details: [
'Awesome Company Inc.',
'123 Business Avenue, Suite 100',
'San Francisco, CA 94107',
'[email protected]',
'+1 (555) 123-4567'
],
logo: 'logo.png', // Path to company logo
website: 'www.awesomecompany.com'
},

// Invoice details
invoice: {
number: 'INV-2025-0427',
date: 'April 27, 2025',
dueDate: 'May 27, 2025',
paymentTerms: 'Net 30'
},

// Client information
client: {
name: 'Client Company LLC',
details: [
'Client Company LLC',
'Attn: John Smith',
'456 Client Street',
'New York, NY 10001',
'[email protected]'
]
},

// Invoice items
items: [
{id: 1, description: 'Website Development Services', quantity: 1, amount: 3500.00},
{id: 2, description: 'UI/UX Design', quantity: 1, amount: 1200.00},
{id: 3, description: 'Content Creation', quantity: 10, amount: 800.00},
{id: 4, description: 'Hosting Setup (Annual)', quantity: 1, amount: 240.00}
],

// Tax rate
taxRate: 0.08, // 8%

// Payment information
paymentInfo: [
'Bank: International Bank of Example',
'Account Name: Awesome Company Inc.',
'Account Number: 1234567890',
'Payment Terms: Net 30'
],

// Notes
notes: 'Thank you for your business! Please make payment by the due date.'
};

// Define colors
const colors = {
primary: rgb(0.3, 0.4, 0.6),
lightGray: rgb(0.95, 0.95, 0.95),
gray: rgb(0.8, 0.8, 0.8),
textColor: rgb(0.1, 0.1, 0.1),
white: rgb(1, 1, 1)
};

// Font sizes
const fontSizes = {
title: 24,
heading: 14,
normal: 12,
small: 10
};

async function createInvoice() {
// Create a new PDF document
const pdfDoc = await PDFDocument.create();

// Add a page
const page = pdfDoc.addPage();

// Embed fonts
const fonts = {
bold: await pdfDoc.embedFont(StandardFonts.HelveticaBold),
regular: await pdfDoc.embedFont(StandardFonts.Helvetica),
oblique: await pdfDoc.embedFont(StandardFonts.HelveticaOblique)
};

// Get page dimensions for positioning
const {width, height} = page.getSize();

// Define margins and spacing
const layout = {
leftMargin: 40,
rightMargin: width - 40,
topMargin: height - 140,
headerHeight: 140,
detailLineHeight: 15,
rowHeight: 20,
footerHeight: 40
};

// Draw header background
page.drawRectangle({
x: 0,
y: layout.topMargin,
width: width,
height: layout.headerHeight,
color: colors.primary
});

// Draw company name
page.drawText(invoiceData.company.name, {
x: layout.leftMargin,
y: height - 50,
size: fontSizes.title,
font: fonts.bold,
color: colors.white
});

// Load and embed the logo
try {
const logoBytes = await fs.readFile(invoiceData.company.logo);
const logoImage = await pdfDoc.embedPng(logoBytes);
const logoDims = logoImage.scale(0.04); // Scale down the logo if needed

page.drawImage(logoImage, {
x: layout.leftMargin * 3,
y: height - 135,
width: logoDims.width,
height: logoDims.height
});
} catch (error) {
console.error('Error loading logo:', error);
}

// Draw Invoice text on the right side
const invoiceTextX = width - 230;
page.drawText('INVOICE', {
x: invoiceTextX,
y: height - 50,
size: fontSizes.title,
font: fonts.bold,
color: colors.white
});

// Invoice details on the right
let invoiceDetailY = height - 80;

page.drawText(`Invoice No: ${invoiceData.invoice.number}`, {
x: invoiceTextX,
y: invoiceDetailY,
size: fontSizes.normal,
font: fonts.regular,
color: colors.white
});

invoiceDetailY -= layout.detailLineHeight;
page.drawText(`Date: ${invoiceData.invoice.date}`, {
x: invoiceTextX,
y: invoiceDetailY,
size: fontSizes.normal,
font: fonts.regular,
color: colors.white
});

invoiceDetailY -= layout.detailLineHeight;
page.drawText(`Due Date: ${invoiceData.invoice.dueDate}`, {
x: invoiceTextX,
y: invoiceDetailY,
size: fontSizes.normal,
font: fonts.regular,
color: colors.white
});

// Set up billing sections
const billingY = height - 200;

// Bill To section
page.drawText('BILL TO:', {
x: layout.leftMargin,
y: billingY,
size: fontSizes.heading,
font: fonts.bold,
color: colors.textColor
});

let clientY = billingY - 20;
invoiceData.client.details.forEach((line) => {
page.drawText(line, {
x: layout.leftMargin,
y: clientY,
size: fontSizes.normal,
font: fonts.regular,
color: colors.textColor
});
clientY -= layout.detailLineHeight;
});

// Bill From section
const billFromX = width - 230;
page.drawText('BILL FROM:', {
x: billFromX,
y: billingY,
size: fontSizes.heading,
font: fonts.bold,
color: colors.textColor
});

let companyY = billingY - 20;
invoiceData.company.details.forEach((line) => {
page.drawText(line, {
x: billFromX,
y: companyY,
size: fontSizes.normal,
font: fonts.regular,
color: colors.textColor
});
companyY -= layout.detailLineHeight;
});

// Service/Product table
const tableTop = billingY - 140;
const tableWidth = width - 80;
const colWidths = [40, 280, 100, 100]; // ID, Description, Quantity, Amount

// Table header row
page.drawRectangle({
x: layout.leftMargin,
y: tableTop - layout.rowHeight,
width: tableWidth,
height: layout.rowHeight,
color: colors.lightGray
});

const headers = ['#', 'Description', 'Quantity', 'Amount'];
let xOffset = layout.leftMargin;

headers.forEach((header, i) => {
page.drawText(header, {
x: xOffset + 5,
y: tableTop - 15,
size: fontSizes.normal,
font: fonts.bold,
color: colors.textColor
});
xOffset += colWidths[i];
});

// Table rows
let rowY = tableTop - 40;

invoiceData.items.forEach((item, rowIndex) => {
// Alternating row colors
if (rowIndex % 2 === 1) {
page.drawRectangle({
x: layout.leftMargin,
y: rowY - 5,
width: tableWidth,
height: layout.rowHeight,
color: colors.lightGray
});
}

let rowX = layout.leftMargin;

// Item ID
page.drawText(item.id.toString(), {
x: rowX + 5,
y: rowY,
size: fontSizes.normal,
font: fonts.regular,
color: colors.textColor
});
rowX += colWidths[0];

// Description
page.drawText(item.description, {
x: rowX + 5,
y: rowY,
size: fontSizes.normal,
font: fonts.regular,
color: colors.textColor
});
rowX += colWidths[1];

// Quantity (right-aligned)
page.drawText(item.quantity.toString(), {
x: rowX + colWidths[2] - 60,
y: rowY,
size: fontSizes.normal,
font: fonts.regular,
color: colors.textColor
});
rowX += colWidths[2];

// Amount (right-aligned with currency formatting)
page.drawText(`${item.amount.toFixed(2)}`, {
x: rowX + colWidths[3] - 60,
y: rowY,
size: fontSizes.normal,
font: fonts.regular,
color: colors.textColor
});

rowY -= layout.rowHeight;
});

// Calculate totals
const subtotal = invoiceData.items.reduce((sum, item) => sum + item.amount, 0);
const tax = subtotal * invoiceData.taxRate;
const total = subtotal + tax;

// Summary section with totals
const summaryX = 350;
const amountX = width - 100;
rowY -= 10;

page.drawText('Subtotal:', {
x: summaryX,
y: rowY,
size: fontSizes.normal,
font: fonts.regular,
color: colors.textColor
});

page.drawText(`${subtotal.toFixed(2)}`, {
x: amountX,
y: rowY,
size: fontSizes.normal,
font: fonts.regular,
color: colors.textColor
});

rowY -= layout.rowHeight;

page.drawText(`Tax (${(invoiceData.taxRate * 100).toFixed(0)}%):`, {
x: summaryX,
y: rowY,
size: fontSizes.normal,
font: fonts.regular,
color: colors.textColor
});

page.drawText(`${tax.toFixed(2)}`, {
x: amountX,
y: rowY,
size: fontSizes.normal,
font: fonts.regular,
color: colors.textColor
});

// Draw line above totals
page.drawLine({
start: {x: 330, y: rowY - 10},
end: {x: layout.rightMargin, y: rowY - 10},
thickness: 1,
color: colors.gray
});

rowY -= 30;

page.drawText('Total:', {
x: summaryX,
y: rowY,
size: fontSizes.normal,
font: fonts.bold,
color: colors.textColor
});

page.drawText(`${total.toFixed(2)}`, {
x: amountX,
y: rowY,
size: fontSizes.normal,
font: fonts.bold,
color: colors.textColor
});

// Draw line
page.drawLine({
start: {x: layout.leftMargin, y: rowY - 30},
end: {x: layout.rightMargin, y: rowY - 30},
thickness: 1,
color: colors.gray
});

// Payment information
rowY -= 60;
const paymentInfoTitle = 'Payment Information:';

page.drawText(paymentInfoTitle, {
x: layout.leftMargin,
y: rowY,
size: fontSizes.normal,
font: fonts.bold,
color: colors.textColor
});

rowY -= layout.rowHeight;

invoiceData.paymentInfo.forEach((line, index) => {
page.drawText(line, {
x: layout.leftMargin,
y: rowY - (index * 15),
size: fontSizes.small,
font: fonts.regular,
color: colors.textColor
});
});

// Notes
const notesY = rowY - 80;
const notesTitle = 'Notes:';

page.drawText(notesTitle, {
x: layout.leftMargin,
y: notesY,
size: fontSizes.normal,
font: fonts.bold,
color: colors.textColor
});

page.drawText(invoiceData.notes, {
x: layout.leftMargin,
y: notesY - 20,
size: fontSizes.small,
font: fonts.oblique,
color: colors.textColor
});

// Footer with light background
const footerContent = `${invoiceData.company.name} | ${invoiceData.company.website}`;

page.drawRectangle({
x: 0,
y: 0,
width: width,
height: layout.footerHeight,
color: colors.lightGray
});

page.drawText(footerContent, {
x: width / 2 - 120,
y: 15,
size: fontSizes.small,
font: fonts.regular,
color: colors.textColor
});

// Save the PDF
const pdfBytes = await pdfDoc.save();
await fs.writeFile('invoice.pdf', pdfBytes);

console.log('Professional invoice created successfully!');
}

// Handle any potential errors
createInvoice().catch(err => {
console.error('Error creating invoice:', err);
});

This example creates a professional-looking invoice with:

  1. Branded header and logo.
  2. Company information.
  3. Client billing details.
  4. Itemized table with product/service descriptions.
  5. Financial calculations for subtotal, tax, and total amount.
  6. Payment information section.
  7. Professional footer.

The result is a visually appealing invoice that demonstrates how PDF-lib can be used to create real-world business documents:

Professional invoice with company branding, client details, and itemized billing table

Best Practices for PDF Generation with PDF-lib

PracticeDescription
Modular Code DesignCreate reusable components for headers, footers, and common PDF elements.
• Develop functions that can be easily adapted across different documents.
Robust Error Handling• Implement comprehensive try-catch blocks to manage potential issues during PDF creation.
Validate input data and provide meaningful error messages.
Dynamic File ManagementGenerate unique filenames.
Handle directory creation.
• Provide detailed feedback about PDF generation process.
Flexible Content GenerationDesign functions that can create multiple pages.
• Ensure consistent formatting.
• Allow customizable content.
Performance ConsiderationsOptimize PDF generation by minimizing redundant operations.
Efficiently manage resources.
Testing StrategyDevelop unit tests to verify PDF creation.
• Test font embedding.
• Ensure content generation reliability.
• Provide comprehensive test coverage.
Input Validation• Always validate input data.
Sanitize inputs before PDF generation.
Prevent unexpected errors.
Metadata Management• Include relevant document metadata.
• Add title, author, and creation date.
• Improve document tracking.

Conclusion

PDF-lib is a powerful and flexible library for generating PDF documents in Node.js. Its pure JavaScript implementation, rich feature set, and intuitive API make it an excellent choice for PDF generation in modern applications.

By following the examples and best practices in this guide, you can create complex PDF documents that meet your application's needs. Whether you're generating reports, creating forms, or building document automation tools, PDF-lib provides the capabilities you need without the complexity of native dependencies.

Remember that while PDF-lib is excellent for programmatic PDF generation, you might want to consider alternatives for specific use cases:

  • HTML to PDF conversion: Consider tools like Puppeteer or Playwright.
  • Complex layouts: For more complex layouts, a template-based approach might be easier.
  • High-volume generation: For extremely high-volume PDF generation, specialized HTML to PDF API services might offer better performance and scalability.

Remember: life is like a PDF – sometimes you need to add a little structure to make everything look good! 🎨