pages-to-pdf
v0.3.9
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
paged-with-floats 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.
With paged-with-floats (default)
import {emitPdfFromWindow} from "pages-to-pdf"
import {printHTML, renderHTML, htmlToPDF} from "paged-with-floats"
// One-shot helper provided by paged-with-floats:
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 library only needs the selectors and link-prefix values that match Vivliostyle's output
See the demo/vivliostyle/ folder of this repository:
import {emitPdfFromWindow, type BackendConfig} from "pages-to-pdf"
import {printHTML} from "vivliostyle/print"
const vivliostyleBackend: BackendConfig = {
pageSelector: "[data-vivliostyle-page-container]",
filterEmptyPages: true,
unhidePagesSelector: "[data-vivliostyle-page-container]",
internalLinkPrefix: /^viv-id-.*:0023/,
scaleRunsToMeasuredWidth: true,
handleHyphenation: true
}
printHTML(html, {
removeIframe: false,
printCallback: iframeWindow => {
void (async () => {
const bytes = await emitPdfFromWindow(iframeWindow, {
backend: vivliostyleBackend,
sourceHtml: html,
metadata: {title: "My document"}
})
// download or upload the bytes
iframeWindow.frameElement?.remove()
})()
}
})@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 PAGED_WITH_FLOATS_BACKEND.
import {
emitPdfFromWindow,
PAGED_WITH_FLOATS_BACKEND,
type BackendConfig
} from "pages-to-pdf"
// Default backend (paged-with-floats)
await emitPdfFromWindow(win)
// Explicit default
await emitPdfFromWindow(win, {backend: PAGED_WITH_FLOATS_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 paged-with-floats). |
| 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 PAGED_WITH_FLOATS_BACKEND.
Architecture
HTML + CSS Paged Media
│
▼ pagination engine (paged-with-floats, @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
• 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.
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). - No tagged PDF accessibility structure.
- Browser-inserted hyphenation is only synthesized when
handleHyphenation: trueis enabled.
WOFF2 support
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.
Licenses
- Code: LGPL-3.0 (see
LICENSE). - Bundled fonts: see
public/fonts/OFL.txtand the individualNOTICE-*.txtfiles.
