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-forgeQuickstart
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.
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
Fonts can be file paths or in-memory Buffer values. Each variant (normal / bold / italics / bolditalics) is independent.
await htmlToPdf(html, {
fonts: {
Inter: {
normal: './fonts/Inter-Regular.ttf',
bold: './fonts/Inter-Bold.ttf',
},
},
defaultFont: 'Inter',
});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.
<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. Supports any bwip-js symbology.
<pdf-barcode type="code128" value="ABC-12345" width="220" height="70" />Merge
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');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 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