npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

pages-to-pdf

v0.3.9

Published

Browser-only DOM-to-PDF emitter for CSS Paged Media output

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-pdf

Usage

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 file

Develop

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 tests

Features

  • Word-precise text positioning preserving browser layout.
  • Embedded subsetted fonts from @font-face rules 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 ::marker and @page margin-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-lib limitations).
  • No tagged PDF accessibility structure.
  • Browser-inserted hyphenation is only synthesized when handleHyphenation: true is 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:

  1. EmitOptions.woff2WasmUrl (string URL or raw ArrayBuffer).
  2. <EmitOptions.baseUrl>woff2/woff2.wasm.
  3. In Node, fonteditor-core resolves its own packaged woff2.wasm.

Licenses

  • Code: LGPL-3.0 (see LICENSE).
  • Bundled fonts: see public/fonts/OFL.txt and the individual NOTICE-*.txt files.