html-pdf vs html2pdf.js vs jspdf vs pdfmake
Generating PDFs in JavaScript: Architecture, Rendering Engines, and Trade-offs
html-pdfhtml2pdf.jsjspdfpdfmakeSimilar Packages:

Generating PDFs in JavaScript: Architecture, Rendering Engines, and Trade-offs

html-pdf, html2pdf.js, jspdf, and pdfmake are four distinct approaches to generating PDF documents within the JavaScript ecosystem. html-pdf acts as a wrapper around PhantomJS to render HTML/CSS server-side, though it is now deprecated. html2pdf.js combines jspdf and html2canvas to capture a visual snapshot of DOM elements in the browser. jspdf is a low-level library for programmatically drawing PDF content (text, lines, images) from scratch. pdfmake uses a custom declarative JSON structure to define document layout, handling pagination and styling internally without relying on HTML/CSS rendering.

Npm Package Weekly Downloads Trend

3 Years

Github Stars Ranking

Stat Detail

Package
Downloads
Stars
Size
Issues
Publish
License
html-pdf03,587-4685 years agoMIT
html2pdf.js04,89110.9 MB4996 months agoMIT
jspdf031,24230.2 MB1204 months agoMIT
pdfmake012,30515.3 MB229a month agoMIT

Generating PDFs in JavaScript: Architecture, Rendering Engines, and Trade-offs

Creating PDFs in JavaScript is a common requirement, but the ecosystem offers vastly different tools depending on whether you need to render HTML, draw shapes manually, or define structured documents. The packages html-pdf, html2pdf.js, jspdf, and pdfmake represent four distinct architectural patterns. Understanding their underlying engines is critical to avoiding performance pitfalls and maintenance nightmares.

⚠️ Critical Warning: The Deprecated Path

html-pdf is the only package in this list that you should avoid entirely for new development. It relies on PhantomJS, a headless browser engine that was discontinued in 2018. The npm package is officially deprecated, and it fails to run on modern Node.js versions without significant hacking. It also carries unpatched security vulnerabilities.

If your legacy system uses html-pdf, you must plan a migration. The modern equivalent for server-side HTML rendering is using Puppeteer or Playwright to control a real Chrome/Chromium instance.

// ❌ DO NOT USE: html-pdf (Deprecated/Unmaintained)
const pdf = require('html-pdf');
pdf.create('<h1>Hello</h1>').toFile('out.pdf', (err, res) => {
  // This will likely fail on Node 18+
});

// ✅ MODERN ALTERNATIVE: Puppeteer (Server-side HTML)
const puppeteer = require('puppeteer');
(async () => {
  const browser = await puppeteer.launch();
  const page = await browser.newPage();
  await page.setContent('<h1>Hello</h1>');
  await page.pdf({ path: 'out.pdf', format: 'A4' });
  await browser.close();
})();

📸 Visual Snapshot vs. Programmatic Drawing

The remaining three active libraries split into two camps: those that convert HTML/DOM to images (html2pdf.js) and those that construct PDFs using native drawing commands or declarative structures (jspdf, pdfmake).

html2pdf.js: The DOM Screenshot Approach

html2pdf.js is a convenience wrapper that chains html2canvas and jspdf. It takes a DOM element, renders it to an HTML5 Canvas (essentially taking a screenshot), and then places that image into a PDF.

  • Pros: Handles complex CSS (gradients, flexbox, custom fonts) automatically because the browser renders it first.
  • Cons: The text becomes an image. Users cannot select or copy text from the resulting PDF. Files can be large, and text may look blurry on high-DPI prints.
// html2pdf.js: Captures the visual look of an element
const element = document.getElementById('invoice-preview');

html2pdf().from(element).save();

// Under the hood, this does:
// 1. html2canvas(element) -> <canvas>
// 2. jspdf.addImage(canvasData) -> PDF

jspdf: The Low-Level Canvas Approach

jspdf does not understand HTML or CSS. It provides a coordinate-based API to draw lines, text, and images. You are responsible for calculating where every pixel goes. If you want a line, you define start/end coordinates. If you want text to wrap, you must calculate the width manually.

  • Pros: Tiny file size, crisp vector text, full programmatic control.
  • Cons: Extremely verbose for complex layouts. No automatic pagination (you must detect when to create a new page).
// jspdf: Manual coordinate drawing
import jsPDF from 'jspdf';

const doc = new jsPDF();

// You must specify exact X, Y coordinates
doc.text('Invoice #123', 10, 10);
doc.line(10, 15, 200, 15); // Draw a line manually

// Text wrapping requires manual calculation or plugins
doc.text('This is a long description that might wrap...', 10, 25, {
  maxWidth: 180 
});

doc.save('invoice.pdf');

pdfmake: The Declarative Document Approach

pdfmake sits in the middle. It doesn't render HTML, but it doesn't require manual coordinates either. You define the document structure using a JSON-like definition (DD). It handles the heavy lifting of layout, table calculations, and pagination automatically.

  • Pros: Excellent for structured data (tables, lists, headers/footers). Clean separation of content and style. Reliable pagination.
  • Cons: Learning its specific syntax. Cannot simply dump existing HTML into it; you must transform your data into its definition format.
// pdfmake: Declarative structure definition
import pdfMake from 'pdfmake/build/pdfmake';
import pdfFonts from 'pdfmake/build/vfs_fonts';

pdfMake.vfs = pdfFonts;

const docDefinition = {
  content: [
    { text: 'Invoice #123', style: 'header' },
    {
      table: {
        body: [
          ['Item', 'Price'],
          ['Widget', '$10.00'],
          ['Gadget', '$25.00']
        ]
      }
    }
  ],
  styles: {
    header: { fontSize: 18, bold: true }
  }
};

pdfMake.createPdf(docDefinition).download('invoice.pdf');

🔄 Handling Pagination and Layout Flow

One of the hardest problems in PDF generation is pagination. How does the library know when to start a new page?

jspdf offers no automatic pagination. If your text runs off the bottom of the page, it simply disappears or overlaps. You must write logic to measure text height and call doc.addPage() manually.

// jspdf: Manual pagination logic required
let y = 10;
const pageHeight = doc.internal.pageSize.height;

longText.split('\n').forEach(line => {
  if (y > pageHeight - 20) {
    doc.addPage();
    y = 20;
  }
  doc.text(line, 10, y);
  y += 10;
});

pdfmake handles pagination automatically. If a table row or paragraph doesn't fit on the current page, it moves the entire block to the next page. You can even keep table headers visible on every page.

// pdfmake: Automatic pagination with repeating headers
const docDefinition = {
  content: [
    {
      table: {
        headerRows: 1,
        body: [
          ['Name', 'Age'], // This repeats on every new page
          ...generate100Rows() // If this overflows, it breaks pages cleanly
        ]
      }
    }
  ]
};

html2pdf.js relies on the browser's print media queries. You can use CSS @media print and page-break-inside: avoid to hint at where breaks should occur, but since it ultimately rasterizes the DOM, complex break logic can sometimes result in cut-off images or awkward spacing.

// html2pdf.js: CSS-based page break hints
const css = `
  .row { page-break-inside: avoid; }
  .new-page { page-break-before: always; }
`;
// Inject CSS into the DOM before capturing

🎨 Styling Capabilities: CSS vs. Custom Definitions

Your choice often depends on where your styling logic lives.

If your team is expert in CSS, html2pdf.js (or a Puppeteer solution) feels natural. You style a div exactly as you want the PDF to look.

/* Used by html2pdf.js */
.invoice-box {
  border: 1px solid #eee;
  box-shadow: 0 0 10px rgba(0,0,0,0.15);
  padding: 30px;
}

If you prefer JavaScript objects and want consistent rendering across server and client without a DOM, pdfmake is superior. You define styles once and apply them by name.

// Used by pdfmake
styles: {
  title: { fontSize: 24, color: '#333', margin: [0, 0, 0, 20] },
  subtotal: { bold: true, fillColor: '#eeeeee' }
}

If you need vector graphics, custom lines, or precise positioning that CSS cannot easily achieve (like drawing a diagonal watermark across the page), jspdf is the only choice.

// Used by jspdf
// Draw a diagonal watermark
doc.setLineWidth(0.5);
doc.setDrawColor(200);
doc.line(0, 0, 210, 297); // A4 dimensions in mm

📊 Summary: Key Differences

Featurehtml2pdf.jsjspdfpdfmakehtml-pdf
Input FormatHTML DOM ElementJS API (Coordinates)JSON Definition (DD)HTML String
Rendering EngineCanvas (Raster)Vector DrawingVector DrawingPhantomJS (Dead)
Text Selectable?❌ No (Image)✅ Yes✅ Yes✅ Yes
PaginationCSS HintsManual CodeAutomaticAutomatic (CSS)
Complex LayoutsEasy (via CSS)Hard (Manual Math)Medium (Table logic)Easy (via CSS)
StatusActiveActiveActiveDeprecated

💡 The Big Picture

Choosing the right tool comes down to your source data and output requirements:

  • Need to export a user's dashboard exactly as seen on screen? Use html2pdf.js. It's the fastest path from DOM to PDF, accepting the trade-off of non-selectable text.
  • Building a robust reporting engine for invoices, contracts, or books? Use pdfmake. Its declarative syntax and automatic pagination make it the most maintainable choice for structured documents.
  • Creating tickets, badges, or documents with custom vector graphics? Use jspdf. It gives you total control, provided you are willing to manage the layout math yourself.
  • Maintaining old code? Migrate away from html-pdf immediately. Switch to Puppeteer for server-side HTML rendering to ensure security and modern CSS support.

Final Thought: There is no "best" library, only the best fit for your data structure. If your data is already in HTML, stick to HTML-based rendering (via Puppeteer or html2pdf). If your data is in a database, transforming it to pdfmake's JSON format often yields the highest quality, professional results.

How to Choose: html-pdf vs html2pdf.js vs jspdf vs pdfmake

  • html-pdf:

    Do NOT choose html-pdf for any new project. It relies on PhantomJS, which has been discontinued since 2018, and the package itself is deprecated. Using it introduces severe security vulnerabilities and compatibility issues with modern Node.js versions. You should immediately migrate existing implementations to puppeteer or playwright if you need server-side HTML rendering.

  • html2pdf.js:

    Choose html2pdf.js when you need a quick, client-side solution to export an existing HTML view (like a dashboard or invoice preview) exactly as it appears on screen. It is ideal for 'what you see is what you get' scenarios where precise pixel-perfect reproduction of complex CSS is required, and you can tolerate potential text blurriness or large file sizes due to canvas rasterization.

  • jspdf:

    Choose jspdf when you need fine-grained, programmatic control over every element in the PDF, such as drawing custom shapes, manipulating raw coordinates, or adding interactive forms. It is best suited for generating simple reports, tickets, or certificates where you build the layout code-by-code rather than relying on HTML/CSS or a declarative schema.

  • pdfmake:

    Choose pdfmake when you need to generate structured, text-heavy documents (like invoices, reports, or books) with reliable pagination, tables, and headers/footers. Its declarative JSON approach separates content from layout logic, making it easier to maintain complex document structures than manual coordinate plotting, while avoiding the fragility of HTML-to-PDF conversion.

README for html-pdf

node-html-pdf

HTML to PDF converter that uses phantomjs

image
Example Business Card
-> and its Source file

Example Receipt

Changelog

Have a look at the releases page: https://github.com/marcbachmann/node-html-pdf/releases

Installation

Install the html-pdf utility via npm:

$ npm install -g html-pdf

Command-line example

$ html-pdf test/businesscard.html businesscard.pdf

Code example

var fs = require('fs');
var pdf = require('html-pdf');
var html = fs.readFileSync('./test/businesscard.html', 'utf8');
var options = { format: 'Letter' };

pdf.create(html, options).toFile('./businesscard.pdf', function(err, res) {
  if (err) return console.log(err);
  console.log(res); // { filename: '/app/businesscard.pdf' }
});

API

var pdf = require('html-pdf');
pdf.create(html).toFile([filepath, ]function(err, res){
  console.log(res.filename);
});

pdf.create(html).toStream(function(err, stream){
  stream.pipe(fs.createWriteStream('./foo.pdf'));
});

pdf.create(html).toBuffer(function(err, buffer){
  console.log('This is a buffer:', Buffer.isBuffer(buffer));
});


// for backwards compatibility
// alias to pdf.create(html[, options]).toBuffer(callback)
pdf.create(html [, options], function(err, buffer){});

Footers and Headers

html-pdf can read the header or footer either out of the footer and header config object or out of the html source. You can either set a default header & footer or overwrite that by appending a page number (1 based index) to the id="pageHeader" attribute of a html tag.

You can use any combination of those tags. The library tries to find any element, that contains the pageHeader or pageFooter id prefix.

<div id="pageHeader">Default header</div>
<div id="pageHeader-first">Header on first page</div>
<div id="pageHeader-2">Header on second page</div>
<div id="pageHeader-3">Header on third page</div>
<div id="pageHeader-last">Header on last page</div>
...
<div id="pageFooter">Default footer</div>
<div id="pageFooter-first">Footer on first page</div>
<div id="pageFooter-2">Footer on second page</div>
<div id="pageFooter-last">Footer on last page</div>

Options

config = {

  // Export options
  "directory": "/tmp",       // The directory the file gets written into if not using .toFile(filename, callback). default: '/tmp'

  // Papersize Options: http://phantomjs.org/api/webpage/property/paper-size.html
  "height": "10.5in",        // allowed units: mm, cm, in, px
  "width": "8in",            // allowed units: mm, cm, in, px
  - or -
  "format": "Letter",        // allowed units: A3, A4, A5, Legal, Letter, Tabloid
  "orientation": "portrait", // portrait or landscape

  // Page options
  "border": "0",             // default is 0, units: mm, cm, in, px
  - or -
  "border": {
    "top": "2in",            // default is 0, units: mm, cm, in, px
    "right": "1in",
    "bottom": "2in",
    "left": "1.5in"
  },

  paginationOffset: 1,       // Override the initial pagination number
  "header": {
    "height": "45mm",
    "contents": '<div style="text-align: center;">Author: Marc Bachmann</div>'
  },
  "footer": {
    "height": "28mm",
    "contents": {
      first: 'Cover page',
      2: 'Second page', // Any page number is working. 1-based index
      default: '<span style="color: #444;">{{page}}</span>/<span>{{pages}}</span>', // fallback value
      last: 'Last Page'
    }
  },


  // Rendering options
  "base": "file:///home/www/your-asset-path/", // Base path that's used to load files (images, css, js) when they aren't referenced using a host

  // Zooming option, can be used to scale images if `options.type` is not pdf
  "zoomFactor": "1", // default is 1

  // File options
  "type": "pdf",           // allowed file types: png, jpeg, pdf
  "quality": "75",         // only used for types png & jpeg

  // Script options
  "phantomPath": "./node_modules/phantomjs/bin/phantomjs", // PhantomJS binary which should get downloaded automatically
  "phantomArgs": [], // array of strings used as phantomjs args e.g. ["--ignore-ssl-errors=yes"]
  "localUrlAccess": false, // Prevent local file:// access by passing '--local-url-access=false' to phantomjs
                           // For security reasons you should keep the default value if you render arbritary html/js.
  "script": '/url',        // Absolute path to a custom phantomjs script, use the file in lib/scripts as example
  "timeout": 30000,        // Timeout that will cancel phantomjs, in milliseconds

  // Time we should wait after window load
  // accepted values are 'manual', some delay in milliseconds or undefined to wait for a render event
  "renderDelay": 1000,

  // HTTP Headers that are used for requests
  "httpHeaders": {
    // e.g.
    "Authorization": "Bearer ACEFAD8C-4B4D-4042-AB30-6C735F5BAC8B"
  },

  // To run Node application as Windows service
  "childProcessOptions": {
    "detached": true
  }

  // HTTP Cookies that are used for requests
  "httpCookies": [
    // e.g.
    {
      "name": "Valid-Cookie-Name", // required
      "value": "Valid-Cookie-Value", // required
      "domain": "localhost",
      "path": "/foo", // required
      "httponly": true,
      "secure": false,
      "expires": (new Date()).getTime() + (1000 * 60 * 60) // e.g. expires in 1 hour
    }
  ]

}

The full options object gets converted to JSON and will get passed to the phantomjs script as third argument.
There are more options concerning the paperSize, header & footer options inside the phantomjs script.