Skip to main content
Technology & EngineeringDocument Generation Services247 lines

Jspdf

jsPDF: client-side and server-side PDF generation in JavaScript, tables, images, custom fonts, autotable plugin

Quick Summary20 lines
You are an expert in using jsPDF for generating PDF documents in JavaScript.

## Key Points

- Always use `splitTextToSize()` before rendering text blocks to handle word wrapping within your content width — raw `text()` calls will overflow the page margin silently.
- Track the current Y position manually and check it against page height before each block to insert page breaks, since jsPDF does not handle pagination automatically for freeform content.
- Use the `compress: true` option in the constructor to reduce output file size significantly, especially for documents with embedded images.
- Forgetting that jsPDF uses millimeters by default (when `unit: "mm"`) — passing pixel values produces microscopic or enormous output. Always match your coordinates to the configured unit.

## Quick Example

```bash
# Browser or Node.js
npm install jspdf jspdf-autotable

# For Node.js image support
npm install canvas
```
skilldb get document-generation-services-skills/JspdfFull skill: 247 lines
Paste into your CLAUDE.md or agent config

jsPDF — Document Generation

You are an expert in using jsPDF for generating PDF documents in JavaScript.

Core Philosophy

Overview

jsPDF is a widely used library for generating PDF documents directly in JavaScript, both in the browser and in Node.js. Unlike pdf-lib which operates at the raw PDF specification level, jsPDF provides a higher-level drawing API similar to HTML Canvas — you call methods like text(), rect(), line(), and addImage() to build pages. The jspdf-autotable plugin adds automatic table generation with pagination. Choose jsPDF when you need a straightforward imperative API for building PDFs from scratch, especially for reports and invoices that combine text, tables, and images.

Setup & Configuration

# Browser or Node.js
npm install jspdf jspdf-autotable

# For Node.js image support
npm install canvas
import { jsPDF } from "jspdf";
import autoTable from "jspdf-autotable";

// Basic document creation
function createBasicPdf(): jsPDF {
  // Orientation, unit, format
  const doc = new jsPDF({
    orientation: "portrait",
    unit: "mm",
    format: "a4",         // 210 x 297 mm
    compress: true,
  });

  return doc;
}

// Register a custom font (must be base64-encoded TTF)
import { readFileSync } from "fs";

function registerCustomFont(doc: jsPDF, fontPath: string, fontName: string): void {
  const fontData = readFileSync(fontPath).toString("base64");
  doc.addFileToVFS(`${fontName}.ttf`, fontData);
  doc.addFont(`${fontName}.ttf`, fontName, "normal");
}

Core Patterns

Report with header, body text, and footer

interface ReportConfig {
  title: string;
  subtitle: string;
  body: string[];
  logoPath?: string;
}

function generateReport(config: ReportConfig): Buffer {
  const doc = new jsPDF({ orientation: "portrait", unit: "mm", format: "a4" });
  const pageWidth = doc.internal.pageSize.getWidth();
  const pageHeight = doc.internal.pageSize.getHeight();
  const margin = 20;
  const contentWidth = pageWidth - margin * 2;
  let yPos = margin;

  // --- Header ---
  doc.setFillColor(37, 99, 235); // blue
  doc.rect(0, 0, pageWidth, 40, "F");

  doc.setTextColor(255, 255, 255);
  doc.setFontSize(22);
  doc.text(config.title, margin, 20);

  doc.setFontSize(12);
  doc.text(config.subtitle, margin, 30);

  // Reset text color
  doc.setTextColor(0, 0, 0);
  yPos = 55;

  // --- Body paragraphs with automatic page breaks ---
  doc.setFontSize(11);
  for (const paragraph of config.body) {
    const lines = doc.splitTextToSize(paragraph, contentWidth);
    const blockHeight = lines.length * 6;

    if (yPos + blockHeight > pageHeight - 30) {
      addFooter(doc, pageWidth, pageHeight);
      doc.addPage();
      yPos = margin;
    }

    doc.text(lines, margin, yPos);
    yPos += blockHeight + 8;
  }

  addFooter(doc, pageWidth, pageHeight);

  return Buffer.from(doc.output("arraybuffer"));
}

function addFooter(doc: jsPDF, pageWidth: number, pageHeight: number): void {
  const pageCount = doc.getNumberOfPages();
  doc.setFontSize(9);
  doc.setTextColor(128, 128, 128);
  doc.text(
    `Page ${pageCount}`,
    pageWidth / 2,
    pageHeight - 10,
    { align: "center" }
  );
  doc.text(
    new Date().toLocaleDateString(),
    pageWidth - 20,
    pageHeight - 10,
    { align: "right" }
  );
  doc.setTextColor(0, 0, 0);
}

Tables with jspdf-autotable

interface SalesRow {
  product: string;
  region: string;
  quantity: number;
  revenue: number;
}

function generateSalesReport(data: SalesRow[]): Buffer {
  const doc = new jsPDF();

  doc.setFontSize(18);
  doc.text("Quarterly Sales Report", 14, 22);
  doc.setFontSize(10);
  doc.setTextColor(100);
  doc.text(`Generated: ${new Date().toISOString().slice(0, 10)}`, 14, 30);
  doc.setTextColor(0);

  autoTable(doc, {
    startY: 38,
    head: [["Product", "Region", "Quantity", "Revenue"]],
    body: data.map((row) => [
      row.product,
      row.region,
      row.quantity.toLocaleString(),
      `$${row.revenue.toLocaleString("en-US", { minimumFractionDigits: 2 })}`,
    ]),
    foot: [[
      "Total", "",
      data.reduce((s, r) => s + r.quantity, 0).toLocaleString(),
      `$${data.reduce((s, r) => s + r.revenue, 0).toLocaleString("en-US", { minimumFractionDigits: 2 })}`,
    ]],
    headStyles: { fillColor: [37, 99, 235], fontSize: 10 },
    footStyles: { fillColor: [240, 240, 240], textColor: [0, 0, 0], fontStyle: "bold" },
    alternateRowStyles: { fillColor: [248, 250, 252] },
    styles: { fontSize: 9, cellPadding: 4 },
    columnStyles: {
      2: { halign: "right" },
      3: { halign: "right" },
    },
    // Hook: add page numbers on every new page
    didDrawPage: (data) => {
      const pageCount = doc.getNumberOfPages();
      doc.setFontSize(8);
      doc.text(
        `Page ${pageCount}`,
        doc.internal.pageSize.getWidth() / 2,
        doc.internal.pageSize.getHeight() - 8,
        { align: "center" }
      );
    },
  });

  return Buffer.from(doc.output("arraybuffer"));
}

Embedding images

import { readFileSync } from "fs";

function addImageToPdf(doc: jsPDF, imagePath: string, x: number, y: number, width: number): void {
  const imageData = readFileSync(imagePath);
  const base64 = imageData.toString("base64");

  // Detect format from extension
  const ext = imagePath.split(".").pop()?.toUpperCase();
  const format = ext === "PNG" ? "PNG" : "JPEG";

  doc.addImage(base64, format, x, y, width, 0); // height=0 preserves aspect ratio
}

Express endpoint

import express from "express";

const app = express();
app.use(express.json());

app.post("/api/report", (req, res) => {
  try {
    const pdf = generateReport(req.body);
    res.setHeader("Content-Type", "application/pdf");
    res.setHeader("Content-Disposition", 'attachment; filename="report.pdf"');
    res.send(pdf);
  } catch (err: any) {
    res.status(500).json({ error: err.message });
  }
});

Best Practices

  • Always use splitTextToSize() before rendering text blocks to handle word wrapping within your content width — raw text() calls will overflow the page margin silently.
  • Track the current Y position manually and check it against page height before each block to insert page breaks, since jsPDF does not handle pagination automatically for freeform content.
  • Use the compress: true option in the constructor to reduce output file size significantly, especially for documents with embedded images.

Common Pitfalls

  • Forgetting that jsPDF uses millimeters by default (when unit: "mm") — passing pixel values produces microscopic or enormous output. Always match your coordinates to the configured unit.
  • Using doc.save("file.pdf") in Node.js — this triggers a browser download dialog and fails server-side. In Node.js, use Buffer.from(doc.output("arraybuffer")) and write the buffer to disk or send it in a response.

Anti-Patterns

Using the service without understanding its pricing model. Cloud services bill differently — per request, per GB, per seat. Deploying without modeling expected costs leads to surprise invoices.

Hardcoding configuration instead of using environment variables. API keys, endpoints, and feature flags change between environments. Hardcoded values break deployments and leak secrets.

Ignoring the service's rate limits and quotas. Every external API has throughput limits. Failing to implement backoff, queuing, or caching results in dropped requests under load.

Treating the service as always available. External services go down. Without circuit breakers, fallbacks, or graceful degradation, a third-party outage becomes your outage.

Coupling your architecture to a single provider's API. Building directly against provider-specific interfaces makes migration painful. Wrap external services in thin adapter layers.

Install this skill directly: skilldb add document-generation-services-skills

Get CLI access →