fast-html-to-image
v0.0.3
Published
Fast, high-fidelity DOM node to image capture
Maintainers
Readme
fast-html-to-image
Screenshot any DOM node, in the browser, with high fidelity.
Give it an element and get back a pixel-accurate PNG of exactly what the user sees: computed styles, web fonts, images, pseudo-elements, shadow DOM, form state, canvas frames, and scroll positions included. Zero runtime dependencies.
Install
npm install fast-html-to-imageUsage
import { captureNode } from "fast-html-to-image";
const result = await captureNode(document.querySelector("#card"));
result.width; // CSS px of the capture rect
result.height;
await result.toPngDataUrl(); // "data:image/png;base64,..."
await result.toBlob(); // PNG Blob (e.g. for clipboard or upload)
await result.toJpegBlob(0.9); // lossy JPEG, encodes 3 to 8x faster than PNG
await result.toCanvas(); // HTMLCanvasElement
await result.toSvgDataUrl(); // the intermediate SVG, before rasterizationBrowser (IIFE)
<script src="https://unpkg.com/fast-html-to-image/dist/index.global.js"></script>
<script>
FastHtmlToImage.captureNode(document.querySelector("#card"));
</script>Region capture
captureRegion screenshots a pixel rectangle of the page in viewport coordinates, so it fits drag-to-select capture. The rect is a plain Rect ({ x, y, width, height }), which is structurally what getBoundingClientRect() returns, so you can pass a DOM rect straight through.
import { captureRegion } from "fast-html-to-image";
const result = await captureRegion({ x: 120, y: 80, width: 400, height: 300 });
// or capture the live box of any element
const result = await captureRegion(element.getBoundingClientRect());captureRegion takes every shared option plus two region-specific ones: root (the element the region is measured against and captured from, defaulting to document.documentElement) and cullMarginPx (how far outside the region offscreen elements are kept before being pruned for speed).
Prewarming
The first capture on a page pays one-time costs (style probes, stylesheet scan, font fetches). prewarm() pays them ahead of time; prewarm(element) also fills the caches for that element so a later capture of it resolves from cache. Safe to call multiple times.
import { prewarm } from "fast-html-to-image";
prewarm(); // at app idle, before the first capture
prewarm(element); // on hover, before capturing that element on clickOptions
captureNode and captureRegion share a common CaptureOptions base and each adds its own target-specific options: CaptureNodeOptions adds clip and bleed; CaptureRegionOptions adds root and cullMarginPx.
Shared (CaptureOptions)
| Option | Type | Default | Description |
| ---------------------- | ------------------------------------- | ------------------------- | -------------------------------------------------------------------------- |
| scale | number | 1 | Multiplier applied to the output raster size |
| pixelRatio | number | window.devicePixelRatio | Device pixel ratio for the output canvas |
| backgroundColor | string | inherited | Fill painted behind the capture; pass "transparent" to keep transparency |
| embedFonts | boolean | true | Inline used @font-face fonts as data URLs |
| filterNode | (element: Element) => boolean | undefined | Return false to exclude an element and its subtree |
| resolveIframeContent | (iframe) => Promise<string \| null> | undefined | Supply an image data URL for a cross-origin iframe |
| timeoutMs | number | 8000 | Per-resource fetch timeout for images, fonts, and url()s |
| abortSignal | AbortSignal | undefined | Cancels an in-flight capture between pipeline stages |
captureNode only (CaptureNodeOptions)
| Option | Type | Default | Description |
| ------- | ------------------ | ----------- | ---------------------------------------------------------------------------------- |
| clip | Rect | undefined | Crop to a rect in the element's border-box coordinate space (CSS px) |
| bleed | number \| "auto" | 0 | Extra padding (CSS px) around the border box so shadows and filters aren't clipped |
captureRegion only (CaptureRegionOptions)
| Option | Type | Default | Description |
| -------------- | --------- | --------------------------- | --------------------------------------------------------------------------------- |
| root | Element | document.documentElement | Element the region is measured against and captured from |
| cullMarginPx | number | see source | How far outside the region offscreen elements are kept before being pruned |
How it works
captureNode runs a read, clone, inline, rasterize pipeline:
- Snapshot
getComputedStylefor every element in the composed tree, diff each against per-tag defaults, and collapse identical style sets into shared generated classes - Deep-clone the element with live state baked in: shadow DOM flattened, form values reflected, canvas and video frames captured, scroll positions reproduced
- Fetch every external resource (images, backgrounds, used
@font-facesources) and inline it as a data URL - Serialize the clone into
<svg><foreignObject>, which browsers render with the real layout engine - Decode the SVG once and draw it to a canvas shared by every output format
Fidelity
Every change runs a Playwright harness that captures 406 fixtures with captureNode and with the browser's native screenshot, then diffs the two with pixelmatch on Chromium, WebKit, and Firefox. On Chromium, 398 fixtures hold a 0.005 diff budget and most score exactly 0; the exceptions are form controls and transformed roots, where native rendering legitimately differs inside foreignObject.
npm run build && npm testBenchmarks
npm run bench compares captureNode against snapdom, modern-screenshot, html-to-image, html2canvas, and dom-to-image-more on ten fixtures. The harness also fidelity-scores every capture, so read the speed columns together with the fidelity column: a fast capture that renders the wrong pixels is not comparable to an accurate one.
Cross-origin iframes
Cross-origin iframe content is unreadable from the parent page. Two escape hatches exist before the gray-placeholder fallback:
resolveIframeContentoption: supply an image data URL for an iframe yourselfenableIframeBridge(): call inside the framed page (it must also load this library); the parent capture then requests a capture overpostMessage
// inside the cross-origin iframe's page
import { enableIframeBridge } from "fast-html-to-image";
const disableBridge = enableIframeBridge();Known limitations
- Cross-origin iframes without the bridge or hook render as gray placeholders sized to their box
backdrop-filteronly sees content inside the capture root; content behind the root is not captured
Development
npm install
npx playwright install # browsers for the fidelity suite
npm run build
npm run test:unit
npm test # unit + fidelity suite
npm run check # lint + formatLicense
MIT
