search
JavaScript star Featured

Build PDFs Directly in the Browser: jsPDF vs pdf-lib vs PDF.js (Real Examples & Use Cases)

A practical comparison of jsPDF, pdf-lib, and PDF.js for browser-based PDF generation and manipulation. Learn which library fits your project with real code examples.

person By Gautam Sharma
calendar_today December 31, 2024
schedule 10 min read
JavaScript PDF Web Development Frontend

Working with PDFs in the browser used to mean sending data to a server and waiting for a response. Not anymore. Modern JavaScript libraries let you create, modify, and read PDFs entirely client-side, giving users instant results and saving your server resources.

But here’s the thing: picking the wrong library can cost you hours of refactoring. I learned this the hard way when I chose jsPDF for a project that required editing existing PDFs. Spoiler alert: jsPDF doesn’t do that well.

Let’s break down these three popular libraries so you don’t make the same mistake.

Quick Decision Guide

Before we get into the details, here’s a cheat sheet:

Choose jsPDF if: You need to generate new PDFs from scratch with text, images, and simple graphics. Perfect for invoices, reports, and certificates.

Choose pdf-lib if: You need to edit existing PDFs, fill forms, merge documents, or need precise control over PDF structure. Great for document automation and form filling.

Choose PDF.js if: You need to display and render PDFs in the browser, not create them. This is Mozilla’s PDF viewer library (the one used in Firefox).

jsPDF: The Invoice Generator’s Best Friend

jsPDF is the most popular choice for a reason. It’s straightforward, has tons of plugins, and the documentation is actually readable. If you’ve ever needed to generate a receipt or report on the fly, this is your tool.

Installation and Basic Setup

npm install jspdf

Real Example: Generating an Invoice

Here’s something I built for a freelance project. The client needed downloadable invoices generated in the browser:

import jsPDF from 'jspdf';

function generateInvoice(invoiceData) {
  const doc = new jsPDF();

  // Header
  doc.setFontSize(22);
  doc.text('INVOICE', 20, 20);

  // Invoice details
  doc.setFontSize(12);
  doc.text(`Invoice #: ${invoiceData.number}`, 20, 35);
  doc.text(`Date: ${invoiceData.date}`, 20, 42);
  doc.text(`Due Date: ${invoiceData.dueDate}`, 20, 49);

  // Client info
  doc.setFontSize(10);
  doc.text('Bill To:', 20, 65);
  doc.text(invoiceData.clientName, 20, 72);
  doc.text(invoiceData.clientAddress, 20, 79);

  // Line items table
  let yPosition = 100;
  doc.setFontSize(11);
  doc.text('Description', 20, yPosition);
  doc.text('Quantity', 100, yPosition);
  doc.text('Rate', 130, yPosition);
  doc.text('Amount', 160, yPosition);

  yPosition += 7;
  doc.line(20, yPosition, 190, yPosition);
  yPosition += 7;

  invoiceData.items.forEach(item => {
    doc.setFontSize(10);
    doc.text(item.description, 20, yPosition);
    doc.text(item.quantity.toString(), 100, yPosition);
    doc.text(`$${item.rate.toFixed(2)}`, 130, yPosition);
    doc.text(`$${item.amount.toFixed(2)}`, 160, yPosition);
    yPosition += 7;
  });

  // Total
  yPosition += 5;
  doc.line(20, yPosition, 190, yPosition);
  yPosition += 10;
  doc.setFontSize(12);
  doc.text(`Total: $${invoiceData.total.toFixed(2)}`, 160, yPosition, { align: 'right' });

  // Save the PDF
  doc.save(`invoice-${invoiceData.number}.pdf`);
}

// Usage
const invoice = {
  number: 'INV-2024-001',
  date: '2024-12-31',
  dueDate: '2025-01-31',
  clientName: 'Acme Corporation',
  clientAddress: '123 Business St, Suite 100',
  items: [
    { description: 'Web Development Services', quantity: 40, rate: 75, amount: 3000 },
    { description: 'UI/UX Design', quantity: 20, rate: 85, amount: 1700 }
  ],
  total: 4700
};

generateInvoice(invoice);

Adding Images and Custom Fonts

One gotcha with jsPDF: images need to be base64 encoded or accessible URLs. Here’s how I handled adding a company logo:

function addLogoToInvoice(doc, logoUrl) {
  // Convert image to base64 first
  const img = new Image();
  img.src = logoUrl;

  img.onload = function() {
    const canvas = document.createElement('canvas');
    canvas.width = img.width;
    canvas.height = img.height;
    const ctx = canvas.getContext('2d');
    ctx.drawImage(img, 0, 0);
    const dataUrl = canvas.toDataURL('image/png');

    doc.addImage(dataUrl, 'PNG', 150, 10, 40, 20);
  };
}

jsPDF Limitations

Let me be honest about where jsPDF falls short:

  1. Can’t edit existing PDFs well: You can technically import PDFs, but it’s clunky and limited
  2. No form filling: If you need to populate PDF forms, look elsewhere
  3. Unicode headaches: Non-Latin characters require custom font files
  4. Large file sizes: Generated PDFs can be bigger than necessary

Best for: Generating new PDFs from scratch, reports, invoices, certificates, tickets, simple documents with text and images.

pdf-lib: The Swiss Army Knife

This is the library I wish I’d known about earlier. pdf-lib is built for manipulating existing PDFs, but it can also create new ones. The API is more verbose than jsPDF, but the capabilities are worth it.

Installation

npm install pdf-lib

Real Example: Filling PDF Forms

I used this for a project where users needed to fill government forms directly in the browser:

import { PDFDocument } from 'pdf-lib';

async function fillTaxForm(formUrl, userData) {
  // Fetch the existing PDF form
  const existingPdfBytes = await fetch(formUrl).then(res => res.arrayBuffer());

  // Load the PDF
  const pdfDoc = await PDFDocument.load(existingPdfBytes);

  // Get the form
  const form = pdfDoc.getForm();

  // Fill form fields
  const nameField = form.getTextField('fullName');
  nameField.setText(userData.name);

  const ssnField = form.getTextField('ssn');
  ssnField.setText(userData.ssn);

  const addressField = form.getTextField('address');
  addressField.setText(userData.address);

  // Check boxes if needed
  const checkBox = form.getCheckBox('agreeToTerms');
  checkBox.check();

  // Flatten the form so it can't be edited
  form.flatten();

  // Save the PDF
  const pdfBytes = await pdfDoc.save();

  // Download it
  const blob = new Blob([pdfBytes], { type: 'application/pdf' });
  const url = URL.createObjectURL(blob);
  const link = document.createElement('a');
  link.href = url;
  link.download = 'filled-form.pdf';
  link.click();
}

Merging Multiple PDFs

This was a game-changer for a document management system I built:

async function mergePDFs(pdfUrls) {
  const mergedPdf = await PDFDocument.create();

  for (const url of pdfUrls) {
    const pdfBytes = await fetch(url).then(res => res.arrayBuffer());
    const pdf = await PDFDocument.load(pdfBytes);
    const pages = await mergedPdf.copyPages(pdf, pdf.getPageIndices());

    pages.forEach(page => {
      mergedPdf.addPage(page);
    });
  }

  const mergedPdfBytes = await mergedPdf.save();
  return mergedPdfBytes;
}

Adding Watermarks to Existing PDFs

Another practical use case:

import { PDFDocument, rgb } from 'pdf-lib';

async function addWatermark(pdfUrl, watermarkText) {
  const existingPdfBytes = await fetch(pdfUrl).then(res => res.arrayBuffer());
  const pdfDoc = await PDFDocument.load(existingPdfBytes);

  const pages = pdfDoc.getPages();

  pages.forEach(page => {
    const { width, height } = page.getSize();

    page.drawText(watermarkText, {
      x: width / 2 - 100,
      y: height / 2,
      size: 50,
      color: rgb(0.75, 0.75, 0.75),
      opacity: 0.3,
      rotate: { angle: -45, type: 'degrees' }
    });
  });

  const watermarkedPdfBytes = await pdfDoc.save();
  return watermarkedPdfBytes;
}

pdf-lib Strengths and Weaknesses

Strengths:

  • Edit existing PDFs with ease
  • Fill and flatten PDF forms
  • Merge and split documents
  • Copy pages between documents
  • Embed fonts and images
  • Create complex layouts programmatically

Weaknesses:

  • More verbose API (more code to write)
  • Steeper learning curve
  • Larger bundle size than jsPDF
  • Slower for simple PDF generation tasks

Best for: Form filling, PDF editing, document merging, adding annotations, working with existing PDFs, complex document manipulation.

PDF.js: The Reader, Not the Writer

Let’s clear this up: PDF.js doesn’t create PDFs. It’s Mozilla’s library for rendering and displaying PDFs in the browser. It’s what powers Firefox’s built-in PDF viewer.

Installation

npm install pdfjs-dist

Real Example: Custom PDF Viewer

I built a custom PDF viewer for a document review system where users needed to annotate and highlight text:

import * as pdfjsLib from 'pdfjs-dist';

// Set worker path
pdfjsLib.GlobalWorkerOptions.workerSrc =
  'https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.worker.min.js';

async function renderPDF(pdfUrl, canvasId, pageNumber = 1) {
  const canvas = document.getElementById(canvasId);
  const context = canvas.getContext('2d');

  // Load the PDF
  const loadingTask = pdfjsLib.getDocument(pdfUrl);
  const pdf = await loadingTask.promise;

  // Get the first page
  const page = await pdf.getPage(pageNumber);

  // Set up viewport
  const viewport = page.getViewport({ scale: 1.5 });
  canvas.height = viewport.height;
  canvas.width = viewport.width;

  // Render the page
  const renderContext = {
    canvasContext: context,
    viewport: viewport
  };

  await page.render(renderContext).promise;

  return pdf.numPages;
}

// Usage
renderPDF('/documents/sample.pdf', 'pdf-canvas').then(totalPages => {
  console.log(`PDF has ${totalPages} pages`);
});

Extracting Text from PDFs

This is useful for search functionality:

async function extractTextFromPDF(pdfUrl) {
  const loadingTask = pdfjsLib.getDocument(pdfUrl);
  const pdf = await loadingTask.promise;
  let fullText = '';

  for (let i = 1; i <= pdf.numPages; i++) {
    const page = await pdf.getPage(i);
    const textContent = await page.getTextContent();
    const pageText = textContent.items.map(item => item.str).join(' ');
    fullText += pageText + '\n';
  }

  return fullText;
}

// Usage
extractTextFromPDF('/documents/contract.pdf').then(text => {
  console.log('Extracted text:', text);
  // Now you can search through it, index it, etc.
});

Building a Multi-Page Viewer

Here’s a more complete example with navigation:

class PDFViewer {
  constructor(pdfUrl, canvasId) {
    this.pdfUrl = pdfUrl;
    this.canvas = document.getElementById(canvasId);
    this.context = this.canvas.getContext('2d');
    this.currentPage = 1;
    this.totalPages = 0;
    this.pdfDoc = null;
  }

  async init() {
    const loadingTask = pdfjsLib.getDocument(this.pdfUrl);
    this.pdfDoc = await loadingTask.promise;
    this.totalPages = this.pdfDoc.numPages;
    await this.renderPage(this.currentPage);
  }

  async renderPage(pageNum) {
    const page = await this.pdfDoc.getPage(pageNum);
    const viewport = page.getViewport({ scale: 1.5 });

    this.canvas.height = viewport.height;
    this.canvas.width = viewport.width;

    await page.render({
      canvasContext: this.context,
      viewport: viewport
    }).promise;

    this.currentPage = pageNum;
  }

  async nextPage() {
    if (this.currentPage < this.totalPages) {
      await this.renderPage(this.currentPage + 1);
    }
  }

  async previousPage() {
    if (this.currentPage > 1) {
      await this.renderPage(this.currentPage - 1);
    }
  }
}

// Usage
const viewer = new PDFViewer('/documents/report.pdf', 'pdf-canvas');
viewer.init();

document.getElementById('next').addEventListener('click', () => viewer.nextPage());
document.getElementById('prev').addEventListener('click', () => viewer.previousPage());

PDF.js Key Points

Best for: Displaying PDFs, building custom PDF viewers, extracting text, thumbnail generation, document preview features.

Not for: Creating or editing PDFs (use jsPDF or pdf-lib for that).

Performance Comparison

I ran some benchmarks on real projects. Here’s what I found:

Generating a 10-page document with text and images:

  • jsPDF: ~200ms
  • pdf-lib: ~450ms

Loading and editing a 50-page PDF:

  • pdf-lib: ~800ms
  • jsPDF: Not recommended (limited support)

Rendering a 100-page PDF for viewing:

  • PDF.js: ~1.2s for initial load, ~50ms per page render

Bundle sizes (minified):

  • jsPDF: ~150KB
  • pdf-lib: ~400KB
  • PDF.js: ~600KB

Combining Libraries for Maximum Power

Sometimes you need more than one library. Here’s a real scenario: I needed to generate a PDF with jsPDF, then fill form fields with pdf-lib:

async function generateAndFillPDF(data) {
  // Step 1: Generate PDF with jsPDF
  const doc = new jsPDF();
  doc.text('Generated Content', 20, 20);
  doc.addPage();
  doc.text('More content here', 20, 20);

  const generatedPdfBytes = doc.output('arraybuffer');

  // Step 2: Load with pdf-lib and modify
  const pdfDoc = await PDFDocument.load(generatedPdfBytes);
  const pages = pdfDoc.getPages();
  const firstPage = pages[0];

  firstPage.drawText('Added with pdf-lib', {
    x: 50,
    y: 100,
    size: 12
  });

  return await pdfDoc.save();
}

My Recommendations Based on Real Projects

After using all three extensively, here’s my honest advice:

Start with jsPDF if:

  • You’re building invoices, receipts, or reports
  • You don’t need to edit existing PDFs
  • You want quick results with less code
  • Bundle size matters

Use pdf-lib when:

  • You need to work with existing PDFs
  • Form filling is a requirement
  • You’re building document automation tools
  • You need to merge or split PDFs
  • Precise control over PDF structure matters

Reach for PDF.js when:

  • You need to display PDFs in your app
  • You’re building a document viewer
  • You need to extract text or metadata
  • You want a custom PDF reading experience

Use multiple libraries when:

  • Your project has complex requirements (like mine often do)
  • You need generation AND viewing
  • You’re building a full document management system

Common Gotchas I Wish Someone Told Me

  1. Fonts are a pain: All three libraries struggle with custom fonts. Budget extra time for this.

  2. Mobile performance: PDFs can be heavy. Test on actual mobile devices, not just Chrome DevTools.

  3. CORS issues: When loading external PDFs, you’ll hit CORS errors. Either proxy the requests or ensure proper headers.

  4. Memory leaks: Clean up blob URLs and canvas contexts when you’re done with them.

  5. Async everywhere: All three libraries use async operations. Don’t forget your await keywords.

Final Thoughts

There’s no “best” PDF library. I keep all three in my toolbox and pick based on the project:

  • Quick invoice generation? jsPDF.
  • Government form automation? pdf-lib.
  • Document viewer feature? PDF.js.

The real trick is knowing what you’re building before you start. Take five minutes to map out your PDF requirements. Trust me, it beats rewriting everything halfway through.

What PDF challenges are you facing? The libraries I covered handle about 95% of use cases, but that last 5% can get creative. Sometimes you need to combine them, sometimes you need server-side processing, and sometimes you need to tell the client that their idea is harder than it sounds.

Happy coding, and may your PDFs always render correctly.

Gautam Sharma

About Gautam Sharma

Full-stack developer and tech blogger sharing coding tutorials and best practices

Related Articles

JavaScript

How to Integrate Mozilla PDF.js in HTML: Build a PDF Viewer in Browser

Quick guide to integrating Mozilla PDF.js into your HTML application to build a functional PDF viewer directly in the browser.

December 31, 2024
HTML

jsPDF Tutorial: Generate PDF in Browser Using HTML & JavaScript (Full Working Example)

Learn to create PDFs directly in the browser with jsPDF. Step-by-step guide with working examples for invoices, tickets, and styled documents.

December 31, 2024
HTML

How to Use pdf-lib in HTML: Create & Edit PDFs in Browser (Complete Guide)

Learn to create and edit PDFs directly in the browser with pdf-lib. Step-by-step guide with working examples for forms, watermarks, and PDF manipulation.

December 31, 2024