Documentation
Installation, options, templates, watermarks, QR codes, merge and split. For the live, interactive version, head to the playground.
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.
npm install @rexymayderio/html-pdf-forge
# or
bun add @rexymayderio/html-pdf-forgeOptional 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.
# For <pdf-qr> elements (lazy-loaded when first used)
npm install qrcode
# For <pdf-barcode> elements (lazy-loaded when first used)
npm install bwip-jsQuickstart
Pass HTML and an optional options object. Every option has a sensible default, so the smallest call is just htmlToPdf(html).
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');PdfResult
htmlToPdf returns a lazy PdfResult. The underlying pdfmake document only flushes to bytes when one of these methods is called.
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);Options
Every field on HtmlPdfOptions is optional. Per-call options are deep-merged with defaults.
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.
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 preprocessingHtmlPdfForge class
Stateful variant for batch generation with shared defaults. Configure once, generate many. Per-call overrides win on conflicts; nested objects are deep-merged.
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' },
});Page numbers
{current} and {total} are the supported tokens in the format string.
await htmlToPdf(html, {
pageNumber: {
placement: 'footer',
format: '{current} / {total}',
style: { fontSize: 9, alignment: 'center', color: '#666' },
},
});Watermarks
A plain string is treated as { text } with default styling. Pass an object for full control.
// Simple
await htmlToPdf(html, { watermark: 'CONFIDENTIAL' });
// Full options
await htmlToPdf(html, {
watermark: { text: 'DRAFT', color: 'red', opacity: 0.15, angle: -30 },
});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.
// 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.
Metadata
Standard PDF metadata fields, embedded in the PDF info dictionary.
await htmlToPdf(html, {
metadata: {
title: 'Q4 Report',
author: 'Mira Halvorsen',
subject: 'Quarterly numbers',
keywords: ['finance', 'Q4'],
creator: 'Reporting Service',
},
});Protection
Encryption is delegated to PDFKit (which pdfmake uses underneath), so no extra dependency is required.
await htmlToPdf(html, {
protect: {
userPassword: 'open123',
ownerPassword: 'admin456',
permissions: {
printing: 'highResolution',
copying: false,
modifying: false,
},
},
});Templates
Mustache-backed. render() accepts a Promise<data> too, useful for async data sources.
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' },
],
});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.
<pdf-qr value="https://example.com/abc-123" size="120" margin="1" ec="M" />requiredText or URL to encode120Pixel size1Quiet-zone modulesMError correction L | M | Q | HBarcodes
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.
<pdf-barcode type="code128" value="ABC-12345" width="220" height="70" />requiredSymbology (code128, ean13, code39, qrcode, etc.)requiredPayload to encode200Output width in pixels60Output height in pixels3bwip-js scale factortrueRender human-readable text below barcodeMerge
Accepts Buffer, Uint8Array, file paths, or any PdfResult interchangeably.
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');Split
Ranges are 1-indexed and inclusive. Out-of-range or reversed ranges throw PdfSplitError.
import { splitPdf } from '@rexymayderio/html-pdf-forge/split';
const parts = await splitPdf(buffer, [
[1, 3],
[4, 6],
]);
await parts[0].saveToFile('./first.pdf');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.
<!-- 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.
General element properties
Apply on any HTML element to override layout, positioning, or styling at the pdfmake level.
<!-- 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.
<!-- 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>Errors
All errors thrown by html-pdf-forge extend HtmlPdfForgeError.
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
What does NOT work
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.
<!-- ❌ 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.
<!-- 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.
<!-- 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: truein 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