Skip to content
Capabilities

Capabilities

htmlpdfx has two rendering tiers, and they are deliberately not equal. Knowing which one you're on is the difference between "my PDF matches my webpage" and "close enough".

Chromium

high fidelity

The browser's own print-to-PDF, via Puppeteer (bundled) or Playwright. The PDF is the webpage, rendered by the same engine — so page breaks, fonts, CSS, and layout are correct by construction. Runs on Node, Bun, and Deno, and is the default whenever a Chromium driver is available.

Use this for anything user-facing.

Canvas

best effort

A zero-dependency DOM rasterizer for the browser when Chromium isn't available. It produces a faithful screenshot of your DOM, sliced cleanly across pages — but it is a raster, not a reflow.

Use it as graceful degradation, not as a replacement for the real engine.

What each tier supports

Every row says why the two differ, because "partial" on its own isn't useful information.

23 capabilities

Chromium

High fidelity

19 of 23 full

Canvas

Best effort

10 of 23 full

Text and vectors

Selectable, searchable text

The canvas tier rasterizes body text into the page image, so it can't be selected, searched, or copied.

Yes
No

Vector graphics, crisp at any zoom

Canvas embeds a raster image, so zooming in shows pixels.

Yes
No

Clickable links

<a href> becomes a real PDF link annotation in Chromium; it's flattened into the image in canvas.

Yes
No

Unicode and i18n

Any glyph the browser can render. PDF strings are escaped safely in both tiers.

Yes
Yes

RTL and bidirectional text

Chromium uses the real bidi engine. Canvas depends on what the host browser resolves before rasterizing.

Yes
Partial

Pagination

Native CSS page breaks

break-before, break-after, break-inside and @page are honoured by a real layout engine in Chromium. Canvas approximates them with a measured slicer.

Yes
Partial

Never splits an atomic block

Both tiers back up to the top of a table row, card, or figure rather than cutting through it.

Yes
Yes

Repeating table headers

<thead> repeats on every page a table spans — free in Chromium via table-header-group.

Yes
No

No blank pages

Page count is exactly ceil(content / page), with no trailing blank.

Yes
Yes

Headers and footers

{{page}}, {{pages}} and {{title}} tokens. Selectable text in Chromium; rasterized in canvas.

Yes
Yes

CSS and layout

Modern CSS — grid, flexbox, oklch()

Canvas depends on the host browser's foreignObject support for advanced CSS.

Yes
Partial

Web fonts

document.fonts.ready is awaited before rendering, and fonts are embedded.

Yes
Yes

Background colours and images

print-color-adjust: exact is forced, so backgrounds survive.

Yes
Yes

Page size and orientation

Never silently auto-flipped — an explicit custom size is preserved as given.

Yes
Yes

Document features

Outline and bookmarks

Generated from heading structure with chromium: { outline: true }.

Yes
No

Tagged, accessible output (PDF/UA)

On by default in the Chromium tier.

Yes
No

Interactive form fields

AcroForm widgets aren't implemented yet. Tracked on the roadmap.

No
No

Password and encryption

User and owner passwords and permission flags aren't implemented yet. Tracked on the roadmap.

No
No

Running it

Runs server-side, headless

The canvas tier needs a DOM, so it's browser-only.

Yes
No

Runs in the browser

Exactly the inverse — this is why auto picks per runtime.

No
Yes

Warm-browser pooling

createRenderer() keeps one browser open across renders.

Yes
No

Configurable DPI and scale

Canvas defaults to 2× device scale.

Yes
Yes

No install required

Puppeteer ships as an optional dependency, so Chromium arrives with it — but it is a download. The canvas tier needs nothing.

Partial
Yes

Check the tier at runtime

The same matrix is available programmatically, so you can branch, warn, or degrade a feature instead of shipping a surprise:

ts
import { engineCapabilities, describeEngines } from "htmlpdfx";

engineCapabilities.chromium.selectableText; // true
engineCapabilities.canvas.tier; // "best-effort"
describeEngines(); // one-line summary for a log or CLI

Or read it off a finished render:

ts
const pdf = await toPdf(html);

if (pdf.engine === "canvas") {
  console.warn("Rasterized fallback — text in this PDF is not selectable.");
}

Not implemented yet

These are real gaps rather than partial support. They're additive, so they can land in a 1.x release:

Form fields

Interactive AcroForm widgets.

Encryption

User and owner passwords, and permission flags.

<PdfExport>

A browser component for client-side React and Vue export.

How this is verified

Every claim above is checked against a real produced PDF rather than asserted in prose. test/e2e.mjs renders through real Chromium in CI and asserts on the output bytes — page count, extractable text, repeating <thead>, link annotations, a navigable outline, and renderer pooling — while test/cross-browser.mjs drives the canvas tier through Chromium, Firefox, and WebKit.

bash
npm test             # unit + behaviour suite
npm run test:browser # Chromium + Firefox + WebKit rendering
npm run test:e2e     # assert on a real Chromium-produced PDF

Released under the MIT License. Sponsor.