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
§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 / [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.
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

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>`,
});
§06

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' },
  },
});
§07

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 },
});
§08

Custom fonts

Fonts can be file paths or in-memory Buffer values. Each variant (normal / bold / italics / bolditalics) is independent.

ts
await htmlToPdf(html, {
  fonts: {
    Inter: {
      normal: './fonts/Inter-Regular.ttf',
      bold: './fonts/Inter-Bold.ttf',
    },
  },
  defaultFont: 'Inter',
});
§09

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',
  },
});
§10

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,
    },
  },
});
§11

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' },
  ],
});
§12

QR codes

Drop a <pdf-qr> element into your HTML. The pipeline auto-renders it to an embedded image.

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
§13

Barcodes

Drop a <pdf-barcode> element into your HTML. Supports any bwip-js symbology.

html
<pdf-barcode type="code128" value="ABC-12345" width="220" height="70" />
§14

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');
§15

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');
§16

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.
§17

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

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 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