pages-to-pdf
v0.4.1
Published
Browser-only DOM-to-PDF emitter for CSS Paged Media output
Maintainers
Readme
pages-to-pdf
Browser-only DOM-to-PDF emitter for CSS Paged Media output.
pages-to-pdf takes a paginated HTML document (already rendered into page
containers by a CSS Paged Media engine) and re-renders it as a real vector PDF
using @pdfme/pdf-lib. No server, no print
dialog. It is intentionally pagination-engine agnostic: it works with
paginate-for-print by
default and can be configured for other engines such as
@vivliostyle/print.
See the demo for usage examples.
Install
npm install pages-to-pdfUsage
The library exports one main function: emitPdfFromWindow(window, options?).
You supply a Window (usually an iframe contentWindow) whose document has
already been paginated into page containers, plus the optional EmitOptions
object described below. Among other things, EmitOptions takes an optional
onProgress: (message: string) => void callback for status feedback.
With paginate-for-print (default)
import {emitPdfFromWindow} from "pages-to-pdf"
import {printHTML, renderHTML, htmlToPDF} from "paginate-for-print"
// One-shot helper provided by paginate-for-print:
const bytes = await htmlToPDF(html, {title: "My document"})
// Or compose the steps manually:
const iframe = await printHTML(html, {keepIframe: true})
try {
const bytes = await emitPdfFromWindow(iframe.contentWindow!, {
sourceHtml: html,
metadata: {title: "My document"}
})
} finally {
iframe.remove()
}With @vivliostyle/print
The vivliostyle-pdf
package wraps @vivliostyle/print and pins the backend configuration
matching Vivliostyle's output, so switching from paginate-for-print is just a
matter of switching packages:
import {
emitPdfFromVivliostyleWindow,
printHTML
} from "vivliostyle-pdf"
printHTML(html, {
removeIframe: false,
printCallback: iframeWindow => {
void (async () => {
const bytes = await emitPdfFromVivliostyleWindow(iframeWindow, {
sourceHtml: html,
metadata: {title: "My document"}
})
// download or upload the bytes
iframeWindow.frameElement?.remove()
})()
}
})For a fully custom Vivliostyle configuration you can also compose the steps
yourself with emitPdfFromWindow and a backend configuration describing
Vivliostyle's DOM output (see the demo/vivliostyle/ folder of this
repository for a working example).
@vivliostyle/print does not await printCallback, so removeIframe: false
keeps the iframe alive until the asynchronous emitter resolves.
Configuration
The backend option controls how the emitter discovers pages, links, margin
boxes, and asset bases. It is a BackendConfig object. When omitted, the
emitter defaults to PAGINATE_FOR_PRINT_BACKEND.
import {
emitPdfFromWindow,
PAGINATE_FOR_PRINT_BACKEND,
type BackendConfig
} from "pages-to-pdf"
// Default backend (paginate-for-print)
await emitPdfFromWindow(win)
// Explicit default
await emitPdfFromWindow(win, {backend: PAGINATE_FOR_PRINT_BACKEND})
// Custom engine
await emitPdfFromWindow(win, {
backend: {
pageSelector: ".my-page",
filterEmptyPages: false,
scaleRunsToMeasuredWidth: true,
handleHyphenation: true
// ...
}
})BackendConfig fields
| Field | Description |
|---|---|
| pageSelector | CSS selector for page root elements. |
| filterEmptyPages | Drop pages containing no visible text or images. |
| unhidePagesSelector | Selector for hidden page containers to un-hide before measuring. |
| marginBoxSelector | Selector for margin-box pseudo-content (used by paginate-for-print). |
| internalLinkPrefix | String/RegExp prefix to strip from internal fragment IDs (Vivliostyle rewrites anchors). |
| bundleBaseGlobal | Global property on window that holds the font/asset base URL. |
| scaleRunsToMeasuredWidth | Scale each word run horizontally to its measured DOM width. |
| handleHyphenation | Synthesize trailing hyphens for browser-inserted hyphenation. |
| metricCompatibleFonts | Extra metric-compatible fallback font files. |
| metricFamilyAliases | Family aliases that map to metric-compatible buckets. |
| defaultCreator | Default PDF /Creator string. |
The default backend is PAGINATE_FOR_PRINT_BACKEND.
PDF/A and PDF/UA
Pass pdfOptions to enable archival and accessibility conformance:
await emitPdfFromWindow(win, {
pdfOptions: {
pdfA: true, // `true`/`"4"` → PDF/A-4, `"4f"` → PDF/A-4f
pdfUa: true // PDF/UA-2 (tagged PDF with a structure tree)
}
})- Enabling either option produces a PDF 2.0 file with an sRGB output intent
(the
sRGB2014.iccprofile is fetched from<baseUrl>icc/, see below) and XMP metadata declaring the conformance claims. Both flags can be combined for a dual-conformant PDF/A-4 + PDF/UA-2 file. pdfA: "4f"is required instead oftrue/"4"whenever embedded files (theattachmentsoption) are present, since PDF/A-4 forbids embedded files.- PDF/UA-2 tags the content: logical structure, artifacts (page margin
boxes, repeated headers), footnote structure, and annotation flags are
derived from the paginated DOM via the backend's tagging configuration
(
artifactSelectors,wrapperSelectors,footnoteSelectors,logicalElementKey,repeatedHeaderSelector).
Architecture
HTML + CSS Paged Media
│
▼ pagination engine (paginate-for-print, @vivliostyle/print, ...)
paginated DOM (hidden iframe, one container per page)
│
▼ emitPdfFromWindow()
DOM→PDF emitter (src/pdf-emitter.ts)
• measures each page container (px → pt, 1 px = 0.75 pt, Y flipped)
• paints background-color rects and solid/dashed/dotted/double borders
• embeds PNG/JPEG images; draws SVG as vector or rasterizes it
• positions every word from Range.getClientRects() so browser line
breaking and justification are preserved
• synthesizes small caps, list markers, and margin-box content
• attaches link annotations (URI and GoTo actions)
• writes metadata, outlines (h1–h6 bookmarks), viewer preferences,
and optional file attachments
• optionally writes PDF/A-4(-4f) and PDF/UA-2 conformance: output
intent, XMP metadata, tagged structure tree, artifact handling
• resolves @font-face fonts via CSS font matching, embeds them
subsetted, and falls back to bundled fonts when needed
│
▼
Uint8Array → Blob → PDF fileDevelop
pnpm install
pnpm run build # TypeScript compile → lib/
pnpm run demo # start the local demo server (http://localhost:5173/pages-to-pdf/)
pnpm run test:e2e # Playwright end-to-end testsFeatures
- Word-precise text positioning preserving browser layout.
- Embedded subsetted fonts from
@font-facerules with CSS-style matching. - Per-glyph fallback to bundled fallback fonts.
- Metric-compatible fallback fonts for common system font substitutions.
- PNG/JPEG images; SVG as vector or rasterized.
- Synthesized
::markerand@pagemargin-box content. - Internal/external link annotations.
- PDF metadata, outline/bookmarks, viewer preferences, attachments.
- PDF/A-4 / PDF/A-4f and PDF/UA-2 output (tagged structure tree, sRGB output intent, XMP conformance metadata).
Limitations
- Baselines are approximated from font metrics.
- Backgrounds and borders are painted in document order — a simplification of the CSS stacking model.
- Only a subset of border styles and background fills are supported.
- SVG support has some gaps (see
svg4pdf-liblimitations). - Browser-inserted hyphenation is only synthesized when
handleHyphenation: trueis enabled.
Bundled assets
The npm package ships public/woff2/, public/fonts/, and public/icc/.
At runtime these are resolved relative to EmitOptions.baseUrl:
WOFF2
WOFF2 fonts are decoded with fonteditor-core's WASM build of Google's woff2
decoder. The decoder loads lazily and degrades gracefully. The WASM is resolved
in this order:
EmitOptions.woff2WasmUrl(string URL or rawArrayBuffer).<EmitOptions.baseUrl>woff2/woff2.wasm.- In Node,
fonteditor-coreresolves its own packagedwoff2.wasm.
ICC profile
When PDF/A or PDF/UA is enabled, the sRGB output intent profile is fetched
from <baseUrl>icc/sRGB2014.icc. If it cannot be loaded, the output intent
is skipped and the file is written without it.
Fallback fonts
Per-glyph fallback fonts are fetched from <baseUrl>fonts/…. See the
"Licenses" section below.
Licenses
- Code: LGPL-3.0 (see
LICENSE). - Bundled fonts: see
public/fonts/OFL.txtand the individualNOTICE-*.txtfiles. public/icc/sRGB2014.icc: the ICC's sRGB2014 profile, redistributed under the ICC license — seepublic/icc/NOTICE.txt.
