html-pdf-forge
Reference

Documentation

Installation, options, templates, watermarks, QR codes, merge and split. For the live, interactive version, head to the playground.

§01

Install

Requires Node.js 18 or newer. Works in any server environment that can run jsdom — edge runtimes that polyfill DOM APIs are not supported. Use the Node.js runtime.

sh
npm install @rexymayderio/html-pdf-forge
# or
bun add @rexymayderio/html-pdf-forge

Optional dependencies

QR code and barcode features require additional packages. They are lazy-loaded at runtime — only install them if you use <pdf-qr> or <pdf-barcode> elements.

sh
# For <pdf-qr> elements (lazy-loaded when first used)
npm install qrcode

# For <pdf-barcode> elements (lazy-loaded when first used)
npm install bwip-js
§02

Quickstart

Pass HTML and an optional options object. Every option has a sensible default, so the smallest call is just htmlToPdf(html).

tsreport.ts
import { htmlToPdf } from '@rexymayderio/html-pdf-forge';

const pdf = await htmlToPdf('<h1>Q4 Report</h1>', {
  page: {
    size: 'A4',
    orientation: 'portrait',
    margins: { top: 40, right: 40, bottom: 40, left: 40 },
  },
  metadata: { title: 'Q4 Report', author: 'Mira Halvorsen' },
  header: '<div style="text-align:right">Q4 Report</div>',
  pageNumber: { placement: 'footer', format: 'Page {current} of {total}' },
});

await pdf.saveToFile('./q4-report.pdf');
§03

PdfResult

htmlToPdf returns a lazy PdfResult. The underlying pdfmake document only flushes to bytes when one of these methods is called.

ts
await result.toBuffer();    // Node.js Buffer
result.toStream();          // ReadableStream (for piping)
await result.toBase64();    // base64 string
await result.toBlob();      // Blob (browser / Node 18+)
await result.saveToFile(path);
§04

Options

Every field on HtmlPdfOptions is optional. Per-call options are deep-merged with defaults.

OptionTypeNotes
page.sizePageSizeA4 / LETTER / LEGAL / TABLOID / [w, h]. Default A4.
page.orientation'portrait' | 'landscape'Default portrait.
page.margins{ top, right, bottom, left }All optional. Default 40pt.
stylesRecord<tag, pdfmake style>Merged with sensible defaults.
resetStylesbooleanDrops the default style map entirely.
fontsRecord<name, FontDefinition>File paths or Buffers.
defaultFontstringDefaults to bundled Roboto. Use 'Helvetica' or 'Inter' for built-in web fonts (lazy-loaded).
header / footerstring | (page, total) => stringStatic or per-page.
pageNumber{ placement, format, style }header / footer / none.
metadataPdfMetadataEmbedded in PDF info dictionary.
watermarkstring | WatermarkOptionsDiagonal text watermark.
protectProtectOptionsPasswords & permission flags.
converterOptionsRecord<string, unknown>Pass-through to html-to-pdfmake.
§05

Subpath imports

Heavy features are available as separate entry points so you can import only what you need. The QR and barcode modules are lazy-loaded — they only require their peer dependencies when actually invoked.

ts
import { htmlToPdf } from '@rexymayderio/html-pdf-forge';           // Core PDF generation
import { mergePdfs } from '@rexymayderio/html-pdf-forge/merge';     // PDF merging
import { splitPdf } from '@rexymayderio/html-pdf-forge/split';      // PDF splitting
import { inlineQrCodes } from '@rexymayderio/html-pdf-forge/qr';    // QR preprocessing
import { inlineBarcodes } from '@rexymayderio/html-pdf-forge/barcode'; // Barcode preprocessing
§06

HtmlPdfForge class

Stateful variant for batch generation with shared defaults. Configure once, generate many. Per-call overrides win on conflicts; nested objects are deep-merged.

ts
import { HtmlPdfForge } from '@rexymayderio/html-pdf-forge';

const forge = new HtmlPdfForge({
  page: { size: 'A4' },
  metadata: { author: 'Reporting Service' },
  fonts: {
    Inter: {
      normal: './fonts/Inter-Regular.ttf',
      bold: './fonts/Inter-Bold.ttf',
    },
  },
  defaultFont: 'Inter',
});

const a = await forge.generate('<h1>Doc 1</h1>');
const b = await forge.generate('<h1>Doc 2</h1>', {
  metadata: { title: 'Custom Title' },
});
§07

Headers & footers

Static HTML strings or per-page functions. Functions receive the current page number and total page count.

ts
await htmlToPdf(html, {
  header: '<div style="text-align:right">Static header</div>',
  footer: (currentPage, pageCount) =>
    `<div>Page ${currentPage} of ${pageCount}</div>`,
});
§08

Page numbers

{current} and {total} are the supported tokens in the format string.

ts
await htmlToPdf(html, {
  pageNumber: {
    placement: 'footer',
    format: '{current} / {total}',
    style: { fontSize: 9, alignment: 'center', color: '#666' },
  },
});
§09

Watermarks

A plain string is treated as { text } with default styling. Pass an object for full control.

ts
// Simple
await htmlToPdf(html, { watermark: 'CONFIDENTIAL' });

// Full options
await htmlToPdf(html, {
  watermark: { text: 'DRAFT', color: 'red', opacity: 0.15, angle: -30 },
});
§10

Custom fonts

By default, the library uses pdfmake's bundled Roboto font (~3MB VFS). To avoid loading it, specify a built-in web font — these are lazy-loaded from CDN on first use and cached in memory.

ts
// Option 1: Use built-in web fonts (lazy-loaded from CDN, cached in memory)
await htmlToPdf(html, { defaultFont: 'Helvetica' });
// Available: 'Helvetica' (Arimo), 'Inter'

// Option 2: Custom font files
await htmlToPdf(html, {
  fonts: {
    Inter: {
      normal: './fonts/Inter-Regular.ttf',
      bold: './fonts/Inter-Bold.ttf',
    },
  },
  defaultFont: 'Inter',
});

Built-in web fonts: Helvetica (uses Arimo, metrically identical to Helvetica/Arial) and Inter (modern sans-serif). The bundled Roboto VFS is only loaded as a fallback when no defaultFont or custom fonts are provided.

§11

Metadata

Standard PDF metadata fields, embedded in the PDF info dictionary.

ts
await htmlToPdf(html, {
  metadata: {
    title: 'Q4 Report',
    author: 'Mira Halvorsen',
    subject: 'Quarterly numbers',
    keywords: ['finance', 'Q4'],
    creator: 'Reporting Service',
  },
});
§12

Protection

Encryption is delegated to PDFKit (which pdfmake uses underneath), so no extra dependency is required.

ts
await htmlToPdf(html, {
  protect: {
    userPassword: 'open123',
    ownerPassword: 'admin456',
    permissions: {
      printing: 'highResolution',
      copying: false,
      modifying: false,
    },
  },
});
§13

Templates

Mustache-backed. render() accepts a Promise<data> too, useful for async data sources.

ts
import { createTemplate } from '@rexymayderio/html-pdf-forge';

const invoice = createTemplate(`
  <h1>Invoice #{{number}}</h1>
  <p>Bill to: <strong>{{customer}}</strong></p>
  <table>
    {{#items}}
    <tr><td>{{description}}</td><td>{{amount}}</td></tr>
    {{/items}}
  </table>
`);

const pdf = await invoice.render({
  number: 'INV-0042',
  customer: 'Halberd & Folk',
  items: [
    { description: 'Consulting', amount: '$500' },
    { description: 'Design', amount: '$300' },
  ],
});
§14

QR codes

Drop a <pdf-qr> element into your HTML. The pipeline auto-renders it to an embedded image. Requires npm install qrcode — the dependency is lazy-loaded only when a <pdf-qr> element is found.

html
<pdf-qr value="https://example.com/abc-123" size="120" margin="1" ec="M" />
AttributeDefaultNotes
valuerequiredText or URL to encode
size120Pixel size
margin1Quiet-zone modules
ecMError correction L | M | Q | H
§15

Barcodes

Drop a <pdf-barcode> element into your HTML. Requires npm install bwip-js — lazy-loaded only when a <pdf-barcode> element is found. Supports any bwip-js symbology.

html
<pdf-barcode type="code128" value="ABC-12345" width="220" height="70" />
AttributeDefaultNotes
type / bcidrequiredSymbology (code128, ean13, code39, qrcode, etc.)
value / textrequiredPayload to encode
width200Output width in pixels
height60Output height in pixels
scale3bwip-js scale factor
includetexttrueRender human-readable text below barcode
§16

Merge

Accepts Buffer, Uint8Array, file paths, or any PdfResult interchangeably.

ts
import { mergePdfs } from '@rexymayderio/html-pdf-forge/merge';

const merged = await mergePdfs(
  [pdf1, await pdf2.toBuffer(), './third.pdf'],
  { metadata: { title: 'Combined Bundle' } },
);
await merged.saveToFile('./bundle.pdf');
§17

Split

Ranges are 1-indexed and inclusive. Out-of-range or reversed ranges throw PdfSplitError.

ts
import { splitPdf } from '@rexymayderio/html-pdf-forge/split';

const parts = await splitPdf(buffer, [
  [1, 3],
  [4, 6],
]);
await parts[0].saveToFile('./first.pdf');
§18

data-pdfmake attribute

The data-pdfmake attribute lets you pass any valid pdfmake property directly to the underlying document definition. This is how you control things that CSS alone cannot express — like table column widths, row heights, page breaks, and absolute positioning. The value must be a valid JSON string.

Table properties

Apply on the <table> element to control column widths, row heights, and pagination behavior.

PropertyTypeNotes
widthsArray<number | string>Column widths: number (fixed pt), '*' (fill), 'auto' (content), or '30%' (percentage).
heightsnumber | number[]Fixed row height(s) in pt. Single number applies to all rows.
headerRowsnumberHow many rows repeat as headers on each new page.
dontBreakRowsbooleanPrevents a row from being split across pages.
keepWithHeaderRowsnumberHow many body rows to keep on the same page as headers.
html
<!-- Control column widths (number = fixed pt, "*" = fill, "auto" = content) -->
<table data-pdfmake='{"widths": [100, "*", "auto"]}'>
  <tr>
    <td>100pt fixed</td>
    <td>Fill remaining</td>
    <td>Auto width</td>
  </tr>
</table>

<!-- Percentage-based widths -->
<table data-pdfmake='{"widths": ["30%", "70%"]}'>
  <tr>
    <td>30% column</td>
    <td>70% column</td>
  </tr>
</table>

<!-- Fixed row heights -->
<table data-pdfmake='{"widths": ["*", "*"], "heights": 40}'>
  <tr>
    <td>All rows 40pt tall</td>
    <td>Same height</td>
  </tr>
</table>

<!-- Repeat header rows across pages -->
<table data-pdfmake='{"widths": ["*", "*"], "headerRows": 1}'>
  <tr>
    <td style="font-weight:bold; background-color:#f1f5f9;">Name</td>
    <td style="font-weight:bold; background-color:#f1f5f9;">Amount</td>
  </tr>
  <tr><td>Item 1</td><td>$100</td></tr>
</table>

<!-- Prevent row splitting across pages -->
<table data-pdfmake='{"widths": ["*"], "dontBreakRows": true}'>
  <tr><td>This row won't be split across pages</td></tr>
</table>

Cell properties

Apply on <td> or <th> elements to control spanning, borders, and fill.

PropertyTypeNotes
colSpannumberMerge cell across N columns.
rowSpannumberMerge cell across N rows.
border[bool, bool, bool, bool]Control individual borders: [left, top, right, bottom].
fillColorstringBackground color for the cell (hex or color name).
noWrapbooleanDisable word wrap in auto-sized columns.

General element properties

Apply on any HTML element to override layout, positioning, or styling at the pdfmake level.

PropertyTypeNotes
pageBreak'before' | 'after'Force a page break before or after the element.
absolutePosition{ x, y }Position element at exact coordinates (breaks flow).
relativePosition{ x, y }Offset element relative to its normal position.
margin[l, t, r, b] | numberOverride margin for the element.
alignment'left' | 'right' | 'center' | 'justify'Text alignment override.
fontSizenumberOverride font size for the element.
boldbooleanForce bold on the element.
italicsbooleanForce italics on the element.
colorstringOverride text color.
lineHeightnumberOverride line height (e.g. 1.5).
widthnumber | stringElement width (for columns context).
html
<!-- Page break before an element -->
<h2 data-pdfmake='{"pageBreak": "before"}'>New Page Section</h2>

<!-- Page break after an element -->
<p data-pdfmake='{"pageBreak": "after"}'>Content before the break</p>

<!-- Custom HR styling -->
<hr data-pdfmake='{"color": "red", "thickness": 2}'>

<!-- Absolute positioning (use with caution) -->
<p data-pdfmake='{"absolutePosition": {"x": 400, "y": 50}}'>
  Positioned text
</p>

Columns layout

Use data-pdfmake-type="columns" on a <div> to create a pdfmake columns layout. Each direct child becomes a column. Useful for centering tables or side-by-side content without using HTML tables.

html
<!-- Center a table using pdfmake columns -->
<div data-pdfmake-type="columns">
  <div data-pdfmake='{"width": "*"}'></div>
  <div style="width:auto">
    <table>
      <tr><th>Centered Table</th></tr>
      <tr><td>This table is centered on the page</td></tr>
    </table>
  </div>
  <div data-pdfmake='{"width": "*"}'></div>
</div>
§19

Errors

All errors thrown by html-pdf-forge extend HtmlPdfForgeError.

ErrorThrown when
HtmlConversionErrorThe HTML can't be parsed by html-to-pdfmake.
PdfGenerationErrorThe pdfmake printer fails to emit bytes.
FontLoadErrorA font file can't be read or decoded.
ImageProcessingErrorAn image fetch or read fails.
TemplateRenderErrorA Mustache template is malformed.
PdfMergeError / PdfSplitErrorInvalid input for merge/split.
QrCodeRenderError / BarcodeRenderErrorCustom-element rendering fails.
§20

Styling & limitations

html-pdf-forge uses pdfmake (via html-to-pdfmake) — not a browser engine. This means CSS support is limited to what pdfmake can render. Think of it like writing HTML for an email client: inline styles, tables for layout, and no fancy CSS.

What works

FeatureSupported valuesNotes
Inline stylesfont-size, color, font-weight, text-align, margin, background-color, line-height, text-indentApplied directly on elements via style attribute
Tablesborder, border-color, width (with tableAutoSize), text-align, background-colorUse tables for all layout (replaces flex/grid). Use width:100% on table + width:50% on cells for full-width columns.
Text formattingbold, italic, underline, lineThrough, font-size, color, line-heightVia <strong>, <em>, <u>, <del>, <s> or inline style
Lists<ol>, <ul>, <li> with type and start attributesSupports upper-alpha, lower-alpha, upper-roman, lower-roman
Images<img src="https://..." /> or base64 data URIsRemote URLs are fetched automatically. Also supports base64 data URIs. Use width/height attributes for sizing.
Links<a href>External links and internal anchors (href="#id")
Horizontal rule<hr>Renders as a line. Customizable via data-pdfmake attribute.
SVG<svg>Rendered directly as vector graphics

What does NOT work

CSS featureWhy / workaround
<style> blocks / CSS classespdfmake does not parse CSS stylesheets — use inline styles only
padding / padding-*Completely ignored by html-to-pdfmake — use margin instead, or rely on pdfmake default cell spacing
display: flex / gridNot supported — use <table> for multi-column layout
position: absolute / relative / fixedNo CSS positioning — content flows top to bottom
border-radiusNot parsed by the converter
box-shadowNot parsed
border: none / border: 0Does NOT hide table borders — pdfmake renders default black borders. Use border: 0.5px solid #ffffff instead
vertical-alignNot parsed — content aligns to top by default
letter-spacing / word-spacingNot supported
max-width / min-widthNot parsed
overflow / z-indexNo stacking context or overflow control
transform / transition / animationNot supported
floatNot supported — use tables for layout
Custom fonts (without config)Must be explicitly loaded via the fonts option, or use built-in web fonts

Hiding table borders

pdfmake renders borders on all table cells by default. border:none does not work as expected — it still renders black borders. The workaround is to set a white border that blends with the background.

html
<!-- ❌ Does NOT work — renders black borders -->
<table style="border:none;">
  <tr>
    <td style="border:none;">Content</td>
  </tr>
</table>

<!-- ✅ Workaround — white border on white background = invisible -->
<table>
  <tr>
    <td style="border:0.5px solid #ffffff; padding:4px 8px;">Content</td>
  </tr>
</table>

Multi-column layout

Since flex and grid are not supported, use tables with invisible borders for side-by-side content like signature blocks.

html
<!-- Side-by-side layout (e.g. signature block) -->
<table>
  <tr>
    <td style="width:50%; text-align:center; border:0.5px solid #ffffff;">
      <strong>Left Column</strong>
    </td>
    <td style="width:50%; text-align:center; border:0.5px solid #ffffff;">
      <strong>Right Column</strong>
    </td>
  </tr>
</table>

Images

Images support both remote URLs and base64 data URIs. The library fetches remote images automatically and embeds them in the PDF. Supported formats: PNG, JPEG, GIF. Use width/height attributes for sizing.

ts
<!-- Remote URL (fetched automatically) -->
<img src="https://example.com/photo.png" width="120" height="120" />

<!-- Base64 data URI (for bundled assets) -->
<img src="data:image/png;base64,iVBORw0KGgo..." width="120" />

<!-- In Node.js, encode local files at runtime -->
import fs from 'fs';
const logo = fs.readFileSync('./logo.png').toString('base64');
const html = `<img src="data:image/png;base64,${logo}" width="120" />`;

General rules

  • Use inline styles only — no <style> blocks or class selectors
  • Use tables for all layout — they replace flex, grid, and floats
  • padding is ignored — use margin for spacing between elements
  • Use data-pdfmake on tables for column widths — CSS width on tables only works with tableAutoSize: true in converter options
  • Use width:100% on tables that should span the full page, with percentage widths on cells
  • Omit width on tables that should auto-size to content (label-value field tables)
  • Use <br/> for line breaks within table cells
  • Use border: 0.5px solid #ffffff to hide borders (not border:none)
  • Use remote URLs or base64 data URIs for images — both are supported and fetched automatically
  • Test in the playground — what works in a browser may not render identically in the PDF