dompdf.js is a pure-frontend DOM-to-PDF engine powered by TypeScript, Web Workers, Rust, and WebAssembly. It reads the layout already computed by the browser and generates primarily vector-based PDFs directly in the browser, without a server, jsPDF, or a screenshot-based PDF pipeline.
It is designed for browser-side exports that need selectable text, long-document pagination, Chinese and custom fonts, headers and footers, watermarks, forms, compression, or encryption.
In representative long-document benchmarks, dompdf.js can generate a 500-page PDF in about two seconds. With suitable document structure and sufficient device resources, a single export can scale to tens of thousands of pages. Actual rendering time and page limits depend on DOM complexity, image count, font size, compression settings, browser, and device performance.
Live demo: dompdfjs.lisky.com.cn
Migration notes: docs/migration-compat.md
- Features
- Installation
- Quick Start
- API
- Export Options
- Pagination and Page Sizes
- Headers and Footers
- Watermarks
- Fonts and Multilingual Text
- Form Export
- PDF Encryption
- Images and Cross-Origin Resources
- Progress Reporting
- Compatibility and Limitations
- Local Development
- Runs entirely in the browser without uploading the document being exported
- Produces searchable, selectable vector text instead of full-page screenshots
- Uses Web Workers and WASM for PDF rendering to reduce main-thread blocking
- Optimized for large documents: about two seconds for 500 pages in representative benchmarks, with extreme workloads reaching tens of thousands of pages
- Supports both continuous single-page output and standard paginated documents
- Supports A/B/C series, Letter, Legal, Tabloid, and custom page sizes
- Supports headers, footers, page-number placeholders, and per-page configuration
- Supports text and image watermarks, under/over layering, and per-page control
- Supports Unicode, TTF fonts, font subsetting, bold, italic, icon fonts, and language-based font fallback
- Supports images, SVG, Canvas, backgrounds, borders, rounded corners, shadows, opacity, gradients, and other common visual effects
- Preserves hyperlinks as PDF link annotations
- Supports static form appearance and interactive PDF AcroForm fields
- Supports DEFLATE compression, user and owner passwords, and PDF permission flags
- Provides
Blob,Uint8Array, direct-download, and low-level snapshot APIs - Includes TypeScript declarations
dompdf.jsreconstructs the page from geometry already computed by the browser, so CSS layout remains the browser's responsibility. The PDF renderer preserves common visual effects where possible, but it is not a complete browser rendering engine. Complex filters, animations, video, and some advanced CSS may be rasterized or degraded.
npm install dompdf.jsThe runtime requires a modern browser with Web Workers, WebAssembly, Blob, and related Web APIs. The package requires Node.js 18+ for installation, builds, and development, but its export APIs depend on the DOM and cannot be called directly in a plain Node.js environment.
<script src="https://cdn.jsdelivr.net/npm/dompdf.js@latest/dist/dompdf.min.js"></script>When loaded through a <script> tag, the API is exposed globally as dompdf.
The default export returns a Promise<Blob>:
import dompdf from 'dompdf.js';
const element = document.querySelector<HTMLElement>('#capture');
if (!element) throw new Error('Could not find #capture');
const blob = await dompdf(element, {
format: 'a4',
pagination: true,
backgroundColor: '#ffffff',
});
const url = URL.createObjectURL(blob);
window.open(url, '_blank');
// Revoke the URL after the preview has had time to load.
setTimeout(() => URL.revokeObjectURL(url), 30_000);import { downloadPDF } from 'dompdf.js';
const element = document.querySelector<HTMLElement>('#capture');
if (element) {
await downloadPDF(element, {
format: 'a4',
pagination: true,
compress: true,
}, 'report.pdf');
}<button id="export">Export PDF</button>
<section id="capture">Content to export</section>
<script src="https://cdn.jsdelivr.net/npm/dompdf.js@latest/dist/dompdf.min.js"></script>
<script>
document.querySelector('#export').addEventListener('click', async () => {
await dompdf.downloadPDF(
document.querySelector('#capture'),
{ format: 'a4', pagination: true },
'example.pdf',
);
});
</script>The default export and simplest entry point. It behaves the same as exportPDF.
function dompdf(
root: HTMLElement,
options?: ExportOptions,
): Promise<Blob>;Collects the DOM, generates the PDF, and returns a Blob with the MIME type application/pdf. Use it for previews, uploads, or a custom download flow.
import { exportPDF } from 'dompdf.js';
const blob = await exportPDF(element, options);Returns the raw PDF bytes. Use it with the File System API, an upload endpoint, or another binary-processing pipeline.
import { renderToBytes } from 'dompdf.js';
const bytes: Uint8Array = await renderToBytes(element, options);Generates the PDF and triggers a browser download. The default filename is export.pdf.
import { downloadPDF } from 'dompdf.js';
await downloadPDF(element, options, 'invoice.pdf');Returns a WASM-side snapshot summary containing node, image, font, and page counts. It is intended for diagnostics and does not create a downloadable PDF.
import { inspect } from 'dompdf.js';
console.log(await inspect(element, { pagination: true }));The package also exports these lower-level functions:
collectSnapshot(root, options?) -> Promise<Uint8Array>: collect and encode a DOM snapshotcollectSnapshotData(root, options?): return the intermediate data before encodingencodeSnapshot(data, perPageHF, perPageWatermark?) -> Uint8Array: encode intermediate datacomputePageBreaks(root, options?) -> number[]: calculate page-break Y coordinates relative to the root elementpageConfigNeedsPerPageResolutionandwatermarkNeedsPerPageResolution: determine whether a configuration needs per-page resolutionresolvePerPageHFandresolveStaticPageConfigHF: resolve per-page headers and footersresolvePerPageWatermarkandresolveStaticWatermarkPages: resolve per-page watermarks
These APIs are intended for diagnostics, pagination overlays, and custom pipelines. The snapshot binary format is an internal protocol; normal application code should prefer dompdf, exportPDF, renderToBytes, or downloadPDF.
Common methods are also attached to the default export, so these forms are equivalent:
import dompdf, { renderToBytes } from 'dompdf.js';
const bytesA = await dompdf.renderToBytes(element);
const bytesB = await renderToBytes(element);The following options have implemented behavior:
| Option | Type | Default | Description |
|---|---|---|---|
format |
string | [number, number] |
'a4' |
Page-size name or [width, height] in pt |
pageWidthPt |
number |
From format |
Override page width directly |
pageHeightPt |
number |
From format |
Override page height directly |
marginPt |
number | [top, right, bottom, left] |
0 |
PDF margins in pt |
pagination |
boolean |
false |
Paginate using the configured page height |
backgroundColor |
string | null |
null |
Page background; null means transparent |
precision |
number |
2 |
Decimal places retained for PDF coordinates |
compress |
boolean |
false |
Compress PDF streams with DEFLATE |
jpegQuality |
number |
0.85 |
JPEG quality used when converting images |
useCORS |
boolean |
false |
Load cross-origin images with anonymous CORS |
ignoreElements |
(element) => boolean |
None | Skip an element and its content when it returns true |
fontConfig |
FontConfig | FontConfig[] |
None | Register custom fonts |
langFontConfig |
FontConfig[] |
None | Configure Unicode-range font fallback |
pageConfig |
Object or per-page function | See below | Header and footer configuration |
watermark |
Object or per-page function | None | Text or image watermark |
form |
boolean | FormOptions |
Static mode | Form-control export behavior |
encryption |
PdfEncryptionOptions |
None | PDF passwords and permissions |
onProgress |
(progress) => void |
None | Export progress callback |
See src/snapshot.ts and the published package's dist/types directory for the complete TypeScript definitions.
pagination: false: create one content-driven continuous page, useful for receipts, screenshot replacement, and continuous documentspagination: true: create a multi-page PDF using the available area left by the page size, margins, header, and footer
await dompdf(element, {
format: 'a4',
pagination: true,
marginPt: [36, 36, 36, 36],
});For stable pagination, keep the export container's CSS width close to the target page's content width. A4 is approximately 794px x 1123px at 96 DPI; configured margins must be subtracted from that width.
See page_sizes.md for common dimensions. Supported names include:
a0througha10b0throughb10c0throughc10letterandgovernment-letterlegal,junior-legal, andgovernment-legaltabloidandledger
For landscape A4, swap the dimensions explicitly:
await dompdf(element, {
format: [842.25, 595.5],
pagination: true,
});You can also override format with pageWidthPt and pageHeightPt. The orientation option is currently retained only for legacy compatibility and does not swap the page dimensions automatically.
Add a pageBreak attribute to request a page break before an element:
<section>First-page content</section>
<section pageBreak>Start on a new page</section>Add a divisionDisable attribute to keep an element on one page where possible:
<article divisionDisable>
This block will be kept together when possible.
</article>An element taller than the entire available page area may still be split so that the content remains placeable.
await dompdf(element, {
pagination: true,
pageConfig: {
excludePages: [1],
header: {
content: 'Quarterly Operations Report',
height: 48,
contentColor: '#334155',
contentFontSize: 11,
contentPosition: 'centerLeft',
padding: [0, 24, 0, 24],
},
footer: {
content: 'Page ${currentPage} / ${totalPages}',
height: 48,
contentColor: '#64748b',
contentFontSize: 10,
contentPosition: 'center',
padding: [0, 24, 0, 24],
},
},
});content supports these placeholders:
${currentPage}: current page number, starting at 1${totalPages}: total number of pages
contentPosition supports:
center,centerLeft, andcenterRightcenterTopandcenterBottomleftTopandleftBottomrightTopandrightBottom[x, y]for custom coordinates
padding uses the order [top, right, bottom, left]. excludePage accepts one page number or an array, while excludePages accepts an array of page numbers.
When pagination is enabled without an explicit pageConfig, the current version reserves an empty 50px header band and renders ${currentPage}/${totalPages} in a 50px footer. Pass pageConfig explicitly when you need full control.
The function form is called for every page after the total page count is known. Return null to disable the header and footer for a page:
await dompdf(element, {
pagination: true,
pageConfig(pageNum, totalPages) {
if (pageNum === 1) return null;
return {
header: {
content: 'Internal Use Only',
height: 40,
contentPosition: 'rightTop',
},
footer: {
content: `${pageNum} / ${totalPages}`,
height: 40,
contentPosition: 'center',
},
};
},
});The function signature accepted by PageRegionConfig.content is a legacy compatibility entry point and does not receive an operable PDF renderer. For dynamic content, use pageConfig(pageNum, totalPages) and return string content.
await dompdf(element, {
pagination: true,
watermark: {
text: 'INTERNAL ${currentPage}/${totalPages}',
color: 'rgba(185, 28, 28, 0.14)',
fontFamily: 'Helvetica',
fontSize: 28,
fontWeight: 700,
angle: -35,
spacing: [180, 130],
offset: [40, 40],
layer: 'under',
excludePages: [1],
},
});Text-watermark defaults include angle 35, font size 28px, color rgba(0, 0, 0, 0.12), spacing [160, 120], offset [36, 36], and layer under.
await dompdf(element, {
pagination: true,
useCORS: true,
watermark: {
imageUrl: '/assets/company-mark.png',
imageWidth: 120,
opacity: 0.1,
angle: -30,
spacing: [220, 160],
layer: 'over',
},
});Image watermarks support imageWidth, imageHeight, and opacity. If only width or height is supplied, the other dimension is calculated from the source aspect ratio.
await dompdf(element, {
pagination: true,
watermark(pageNum) {
if (pageNum === 1) return null;
return {
text: pageNum % 2 === 0 ? 'EVEN PAGE' : 'ODD PAGE',
color: 'rgba(30, 64, 175, 0.12)',
layer: 'under',
};
},
});For non-Latin text, explicitly embed a TTF font. The most reliable approach is to load the font first and pass it through fontBytes:
import dompdf from 'dompdf.js';
const fontBuffer = await fetch('/fonts/SourceHanSansSC-Regular.ttf').then((response) => {
if (!response.ok) throw new Error(`Font request failed: ${response.status}`);
return response.arrayBuffer();
});
await dompdf(element, {
fontConfig: {
fontFamily: 'SourceHanSansSC-Regular',
fontBytes: new Uint8Array(fontBuffer),
fontStyle: 'normal',
fontWeight: 400,
},
});fontConfig accepts one font or an array. Its main fields are:
| Field | Description |
|---|---|
fontFamily |
Required; should match the exported element's CSS font-family |
fontBytes |
Decoded TTF bytes; recommended |
fontBase64 |
Base64-encoded TTF data |
fontStyle |
normal or italic |
fontWeight |
A weight such as 400 or 700 |
iconFont |
Whether to treat the font as an icon font |
The type definition retains fontUrl, but the current collector does not fetch it. Load URL-based fonts yourself and pass them as a Uint8Array.
langFontConfig selects fonts by Unicode range and can define a default fallback:
await dompdf(element, {
langFontConfig: [
{
fontFamily: 'SourceHanSansSC-Regular',
fontBytes: chineseFontBytes,
charRange: [[0x3400, 0x9fff]],
},
{
fontFamily: 'NotoSans-Regular',
fontBytes: fallbackFontBytes,
isDefault: true,
},
],
});The repository includes examples/SourceHanSansSC-Regular.ttf for local demos and verification. In production, verify the font's license and avoid registering duplicate copies of large complete fonts.
Form values are captured from the DOM state at the start of the export, rather than from the initial HTML attributes.
await dompdf(element, {
pagination: true,
form: {
mode: 'hybrid',
include: [
'text',
'textarea',
'select',
'checkbox',
'radio',
'date-time',
'range',
'color',
'file',
'progress',
'meter',
],
},
});Modes:
static: the default; preserve only the control's current visual appearanceinteractive: create AcroForm fields for controls with a natural PDF mappinghybrid: preserve the static appearance and add interactive fields
Current interactive-field mappings:
- Text-like
inputelements andtextarea selectcheckboxradio
The following types retain a static appearance but do not produce equivalent interactive fields:
date,time,month,week, anddatetime-localrange,color, andfileprogressandmeter
form: true still uses the default static mode. To generate interactive fields, explicitly set mode: 'interactive' or mode: 'hybrid'.
await dompdf(element, {
encryption: {
userPassword: 'reader-password',
ownerPassword: 'owner-password',
userPermissions: ['print', 'copy'],
},
});Available permissions:
print: allow printingmodify: allow modificationscopy: allow copying contentannot-forms: allow annotations and form filling
Unknown permission names cause an error. Whether PDF permissions are strictly enforced ultimately depends on the reader; permission flags are not a substitute for access control over sensitive data.
Same-origin images, data URLs, Canvas content, and readable SVGs can participate directly in an export. Cross-origin images require correct CORS response headers from the resource server:
await dompdf(element, {
useCORS: true,
jpegQuality: 0.9,
});Notes:
useCORS: trueonly requests images with anonymous CORS; it cannot bypass server policy- The image server will normally need to return
Access-Control-Allow-Origin - Wait for images and fonts to finish loading before exporting
- Unreadable images may be skipped or degraded
- Legacy html2canvas options such as
proxy,allowTaint, andimageTimeoutdo not provide their original behavior
You can wait for document fonts and images before exporting:
await document.fonts.ready;
await Promise.all(
Array.from(element.querySelectorAll('img')).map((image) => {
if (image.complete) return Promise.resolve();
return new Promise<void>((resolve) => {
image.addEventListener('load', () => resolve(), { once: true });
image.addEventListener('error', () => resolve(), { once: true });
});
}),
);onProgress reports these stages:
collecting: collecting the DOM and resourcescountingPages: determining the total page count for per-page headers, footers, or watermarksrendering: generating the PDF in WASMdone: export completed
await dompdf(element, {
pagination: true,
onProgress(progress) {
switch (progress.stage) {
case 'collecting':
console.log('Collecting the document');
break;
case 'countingPages':
console.log(`Counting pages: ${progress.totalPages ?? '...'}`);
break;
case 'rendering':
console.log(
`Rendering: ${progress.currentPage ?? 0}/${progress.totalPages ?? '?'}`,
);
break;
case 'done':
console.log(`Export complete: ${progress.totalPages ?? 1} pages`);
break;
}
},
});Not every export emits a countingPages stage. It is used only when a configuration needs the total page count in advance.
The runtime depends on:
- DOM and CSSOM
- Web Workers
- WebAssembly
BlobandURL.createObjectURLTextEncoderandTextDecoder
Use a recent Chromium, Firefox, or Safari release. The library cannot collect a page directly in Node.js, during server-side rendering, or inside a Worker without a DOM. Next.js, Nuxt, and other SSR applications should call the export APIs only on the client.
The project has migrated from html2canvas + jsPDF to DOM snapshot + Worker + WASM. The following options are accepted by the types and runtime, but do not currently provide equivalent legacy behavior. Some emit a warning:
onJspdfReadyandonJspdfFinishforeignObjectRendering,allowTaint,proxy, andimageTimeoutloggingandcachewindowWidth,windowHeight,scrollX, andscrollYx,y,width,height, andscalecanvas,removeContainer, andonclonepdfFileName,floatPrecision,orientation, andputOnlyUsedFonts
ignoreElements does have implemented behavior and can replace some legacy clone-stage filtering. See the migration notes for the full migration path.
- Animations, video, iframes, and browser-plugin content cannot be exported with their full dynamic behavior
- Some complex filters, blend modes, masks, and advanced CSS are degraded or rasterized
- Very high-resolution images and complete CJK fonts increase collection time, memory use, and PDF size
fontUrlandputOnlyUsedFontscurrently have compatibility signatures only; callers must load font URLs themselves- The renderer callback accepted by
PageRegionConfig.contentis a compatibility signature and does not expose an operable jsPDF instance - Code that depends on jsPDF plugins or modifies a live jsPDF instance after export must be migrated
Browser fonts are not embedded into the PDF automatically. Register a TTF containing the required characters through fontConfig.fontBytes, and ensure the element's CSS font-family matches fontFamily.
Keep the export container width close to the target page's content width, and wait for fonts and images to load before exporting. Margins, headers, and footers all reduce the usable area on each page.
Use a custom [width, height] pair or pageWidthPt/pageHeightPt. Do not rely on the currently compatibility-only orientation option.
Set useCORS: true and verify that the image server permits cross-origin reads. A frontend CORS option alone cannot bypass missing server response headers.
Enable compress: true, avoid images far larger than their displayed dimensions, reduce jpegQuality where appropriate, and register only the fonts and weights the document needs.
- The main thread walks the target element and reads browser-computed layout, styles, text, images, and form state.
- TypeScript encodes the collected result into a compact binary snapshot.
- The snapshot is transferred to a Web Worker.
- Rust/WASM performs pagination, font subsetting, drawing, and PDF object generation.
- The main thread receives the PDF bytes and returns a
Uint8Array, aBlob, or starts a download, depending on the API used.
This architecture avoids drawing the entire document into one Canvas and keeps the main PDF-generation work off the main thread.
examples/index.html: comprehensive feature demoexamples/comparison.html: comparison with other frontend PDF approachesexamples/markdown-editor.html: Markdown editing and live export
Run npm run serve from the repository root before opening an example. Do not open examples directly through file://, because browser security policies may block modules, fonts, and cross-origin resources.
- Node.js 18+
- Rust toolchain
wasm32-unknown-unknowntarget
rustup target add wasm32-unknown-unknown
npm install
npm run build
npm test
npm run serveCommon scripts:
| Command | Purpose |
|---|---|
npm run build:wasm |
Compile the Rust/WASM module |
npm run build |
Build the published bundles and TypeScript declarations |
npm run dev |
Watch the source and rebuild continuously |
npm test |
Build WASM and run the PDF smoke verification |
npm run verify |
Same as npm test; run the verification script |
npm run serve |
Start a static server on port 8080 |
npm test currently runs scripts/verify.mjs, which verifies basic pagination, PDF structure, images, Chinese font subsetting, opacity, and composite glyphs.
dompdf.js/
|-- src/ # TypeScript API, DOM collection, Worker, and WASM bridge
|-- wasm/ # Rust pagination, fonts, compression, encryption, and PDF writer
|-- dist/ # Built package artifacts
|-- examples/ # Browser demos and example fonts
|-- docs/ # Migration notes and PDF-diff documentation
`-- scripts/ # Build, verification, and PDF-diff tooling
Read CONTRIBUTING.md before submitting a pull request, and run at least:
npm test
npm run buildReport security issues as described in SECURITY.md. Do not disclose an unpatched vulnerability in a public issue.
See CHANGELOG.md for recent version changes.
This project is licensed under the MIT License.