UNPKG

dom-to-image-more

Version:

Generates an image from a DOM node using HTML5 canvas and SVG

995 lines (767 loc) 47.7 kB
# DOM to Image [![Version](https://img.shields.io/npm/v/dom-to-image-more.svg?style=flat-square)](https://npmjs.com/package/dom-to-image-more) [![Bundle Size](https://img.shields.io/bundlephobia/minzip/dom-to-image-more?style=flat-square)](https://bundlephobia.com/result?p=dom-to-image-more) [![Open Issues](https://img.shields.io/github/issues/IDisposable/dom-to-image-more?style=flat-square)](https://github.com/IDisposable/dom-to-image-more/issues) [![GitHub Repo stars](https://img.shields.io/github/stars/IDisposable/dom-to-image-more?style=social)](https://github.com/IDisposable/dom-to-image-more) [![Twitter](https://img.shields.io/twitter/follow/idisposable.svg?style=social&label=Follow)](https://www.twitter.com/idisposable) ## What is it **dom-to-image-more** is a library which can turn arbitrary DOM node, including same origin and blob iframes, into a vector (SVG) or raster (PNG or JPEG) image, written in JavaScript. This fork of [dom-to-image by Anatolii Saienko (tsayen)](https://github.com/tsayen/dom-to-image) with some important fixes merged. We are eternally grateful for his starting point. Anatolii's version was based on [domvas by Paul Bakaus](https://github.com/pbakaus/domvas) and has been completely rewritten, with some bugs fixed and some new features (like web font and image support) added. Moved to [1904labs organization](https://github.com/1904labs/) from my repositories 2019-02-06 as of version 2.7.3 Moved back from 1904labs organization to [my repositories](https://github.com/IDisposable/) 2026-07-10 as of version 3.10.1 ## 3.10 Breaking Changes The new [`requestInterceptor`](#requestinterceptor) hook unified the internal resource-fetching path. The only change that can affect existing code is at the `impl` surface (which is **not** public API — see the `impl` note under [Using Typescript](#using-typescript) — but is reachable): - `impl.util.getAndEncode(url)` now takes an optional second argument, `getAndEncode(url, type)`, where `type` is a [`ResourceType`](#resource-types). The first argument is unchanged, so existing one-argument calls still work. - [`imagePlaceholder`](#imageplaceholder) is now applied **only to image resources** (`ResourceType.IMAGE` / `ResourceType.CSS_IMAGE`). A failed font or stylesheet — and a direct `getAndEncode(url)` call made **without** a `type` — now drops on failure (resolves to `''`) instead of substituting `imagePlaceholder`, so the CSS fallback (font stack / cascade) applies. Pass `domtoimage.ResourceType.IMAGE` if you call `getAndEncode` directly and want the placeholder. ## What's New ### 3.10.1 Moved back from 1904labs organization to [my repositories](https://github.com/IDisposable/) ### 3.10.0 - **[`requestInterceptor`](#requestinterceptor)** — a single hook to supply or recover any external resource (images, fonts, and stylesheets), consulted both before the fetch and on failure, and the general primitive behind resource handling. Exposes [`domtoimage.ResourceType`](#resource-types) for the resource kind. See the [3.10 Breaking Changes](#310-breaking-changes) note for the related `impl.getAndEncode` / `imagePlaceholder` adjustment. - **[`adjustPseudoElement`](#adjustpseudoelement)** — drop or tweak a `::before`/`::after` pseudo-element as it's recreated in the output (#244). - **[`loadExternalStyleSheet`](#loadexternalstylesheet)** — opt in to fetching and re-parsing cross-origin stylesheets so their `@font-face` web fonts can be discovered and embedded (#243). - **[`ignoreCSSRuleErrors`](#ignorecssruleerrors)** — suppress the `console.error` logged when a cross-origin stylesheet's `cssRules` can't be read during font discovery (#241). - **[`logger`](#logger)** — route the library's `console.warn`/`console.error` diagnostics through a console-like sink to redirect or silence them (#250). ### 3.9.2 - **[`preserveScroll`](#preservescroll)** — reflect each scrollable element's current `scrollLeft`/`scrollTop` instead of rendering everything scrolled to the top/left (opt-in, #22). ### 3.9.1 - Render `::before`/`::after` `url()` backgrounds by inlining pseudo-element styles (#16). - Inline nested SVG `<image>` `href`/`xlink:href` so they survive in the output (#121). - Strip XML-illegal attribute names so malformed HTML still renders (#152). - Neutralize the captured root's margin to stop margin-cropping (#38). - Fail cleanly under SSR with a clear error instead of a raw `ReferenceError` (#83). ### 3.9.0 - **[`pixelRatio`](#pixelratio)** — device-pixel-ratio multiplier for crisp high-DPI/Retina output. - **[`ensureShown`](#ensureshown)** — force a captured root that is hidden by its own `display: none` / `opacity: 0` to render. - Inline CSS `mask`/`mask-image` `url()` so tinted SVG icons render. - Inline external SVG `<use>`/`<symbol>` references into the standalone output. - Fidelity fixes: wrap bare SVG roots in an `<svg>`, stop propagating a parent's `hidden` to children, elide phantom gray borders, and keep table captions from clipping. - Documented the `backdrop-filter` limitation. ### 3.8.0 - **[`filterUrls`](#filterurls)** — filter which `url()` resources get downloaded and inlined. - Ship a bundled TypeScript definition file. - Cross-realm / `<iframe>` type-check fixes. ## Installation ### NPM `npm install dom-to-image-more` Then load ```javascript /* in ES 6 */ import domtoimage from 'dom-to-image-more'; /* in ES 5 */ var domtoimage = require('dom-to-image-more'); ``` ## Usage All the top level functions accept DOM node and rendering options, and return promises, which are fulfilled with corresponding data URLs. Get a PNG image base64-encoded data URL and display right away: ```javascript var node = document.getElementById('my-node'); domtoimage .toPng(node) .then(function (dataUrl) { var img = new Image(); img.src = dataUrl; document.body.appendChild(img); }) .catch(function (error) { console.error('oops, something went wrong!', error); }); ``` Get a PNG image blob and download it (using [FileSaver](https://github.com/eligrey/FileSaver.js/), for example): ```javascript domtoimage.toBlob(document.getElementById('my-node')).then(function (blob) { window.saveAs(blob, 'my-node.png'); }); ``` Save and download a compressed JPEG image: ```javascript domtoimage .toJpeg(document.getElementById('my-node'), { quality: 0.95 }) .then(function (dataUrl) { var link = document.createElement('a'); link.download = 'my-image-name.jpeg'; link.href = dataUrl; link.click(); }); ``` Get an SVG data URL, but filter out all the `<i>` elements: ```javascript function filter(node) { return node.tagName !== 'i'; } domtoimage .toSvg(document.getElementById('my-node'), { filter: filter }) .then(function (dataUrl) { /* do something */ }); ``` Get the raw pixel data as a [Uint8Array](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Uint8Array) with every 4 array elements representing the RGBA data of a pixel: ```javascript var node = document.getElementById('my-node'); domtoimage.toPixelData(node).then(function (pixels) { for (var y = 0; y < node.scrollHeight; ++y) { for (var x = 0; x < node.scrollWidth; ++x) { pixelAtXYOffset = 4 * y * node.scrollHeight + 4 * x; /* pixelAtXY is a Uint8Array[4] containing RGBA values of the pixel at (x, y) in the range 0..255 */ pixelAtXY = pixels.slice(pixelAtXYOffset, pixelAtXYOffset + 4); } } }); ``` Get a canvas object: ```javascript domtoimage.toCanvas(document.getElementById('my-node')).then(function (canvas) { console.log('canvas', canvas.width, canvas.height); }); ``` Adjust cloned nodes before/after children are cloned [sample fiddle](https://jsfiddle.net/IDisposable/grLtjwe5/12/) ```javascript const adjustClone = (node, clone, after) => { if (!after && clone.id === 'element') { clone.style.transform = 'translateY(100px)'; } return clone; }; const wrapper = document.getElementById('wrapper'); const blob = domtoimage.toBlob(wrapper, { adjustClonedNode: adjustClone }); ``` --- _All the functions under `impl` are not public API and are exposed only for unit testing._ _The `impl` surface is described in [docs/IMPL.md](docs/IMPL.md) and its `impl.util` helpers are catalogued in [docs/UTILS.md](docs/UTILS.md)._ --- ### Rendering options #### filter A function taking DOM node as argument. Should return true if passed node should be included in the output (excluding node means excluding it's children as well). Not called on the root node. #### filterStyles A function taking the source node and a style property name as arguments. Should return true if the passed property should be included in the output. Sample use: ```javascript filterStyles(node, propertyName) { return !propertyName.startsWith('--'); // to filter out CSS variables } ``` #### filterUrls A function taking a discovered resource URL and its base URL as arguments, called for each `url(...)` reference found in CSS (backgrounds, masks, `@font-face` sources, etc.). Return `true` to fetch and inline the resource, or `false` to skip it (the original `url(...)` is left untouched). Lets you exclude specific resources from being downloaded. Defaults to undefined (all discovered URLs are processed). Sample use: ```javascript filterUrls(url, baseUrl) { return !url.includes('do-not-inline'); // skip URLs matching a pattern } ``` #### adjustPseudoElement A function to **drop or adjust** a `::before`/`::after` pseudo-element as it is recreated in the output. It receives the source node, which pseudo (`':before'` or `':after'`), and the pseudo-element's computed style, and may return: - **`false`** — drop the pseudo-element (don't recreate it); - **an object of CSS property overrides** (keyed by CSS property name) — apply them on top of the computed style (later declarations win, so they override the defaults); an empty object `{}` is allowed and changes nothing; - **`undefined` / `true`** (or omit the option) — keep the pseudo-element unchanged. It is only called for pseudo-elements that have `content`, so it can adjust an existing pseudo-element but not synthesize one from nothing (use [adjustClonedNode](#adjustclonednode) or [onclone](#onclone) to add real elements). To remove a pseudo-element, return `false` rather than overriding its `content` to `none`/empty (which would still emit an empty rule). Defaults to undefined. Sample use: ```javascript adjustPseudoElement(node, pseudo, style) { // swap an em-dash glyph for a hyphen, drop a decorative overlay, // and leave everything else untouched if (style.getPropertyValue('content').includes('—')) { return { content: '"-"' }; } if (pseudo === ':after' && node.classList.contains('decoration')) { return false; } return undefined; } ``` #### adjustClonedNode A function that will be invoked on each node as they are cloned. Useful to adjust nodes in any way needed before the conversion. Note that this be invoked before the onclone callback. The handler gets the original node, the cloned node, and a boolean that says if we've cloned the children already (so you can handle either before or after) This is a **mutation** hook: adjust the `clone` in place. Any value you return is **ignored** — to swap a node for a different one, use [onclone](#onclone) (which can replace nodes wholesale). Sample use: ```javascript const adjustClone = (node, clone, after) => { if (!after && clone.id === 'element') { clone.style.transform = 'translateY(100px)'; } }; ``` const wrapper = document.getElementById('wrapper'); const blob = domtoimage.toBlob(wrapper, { adjustClonedNode: adjustClone}); #### onclone A function taking the cloned and modified DOM node as argument. It allows to make final adjustements to the elements before rendering, on the whole clone, after all elements have been individually cloned. Note that this will be invoked after all the onclone callbacks have been fired. The cloned DOM might differ a lot from the original DOM, for example canvas will be replaced with image tags, some class might have changed, the style are inlined. It can be useful to log the clone to get a better senses of the transformations. Because `onclone` hands you the whole clone after every node has been processed, it's also the place to **substitute or replace nodes wholesale** — there's no separate per-node "factory" hook because `onclone` already covers it. For example, swap an element that won't rasterize on its own (a WebGL/`<canvas>` you've drawn yourself, a `<video>`, or a custom element — see [Things to watch out for](#things-to-watch-out-for)) with your own image: ```javascript domtoimage.toPng(node, { onclone: (clone) => { clone.querySelectorAll('canvas, video, my-widget').forEach((el) => { const img = new Image(); img.src = myRasterize(el); // e.g. el.toDataURL() for a 2D canvas el.replaceWith(img); }); }, }); ``` To _drop_ a node (and its subtree) entirely, prefer [filter](#filter); to _mutate_ a clone as it's created, prefer [adjustClonedNode](#adjustclonednode). #### bgcolor A string value for the background color, any valid CSS color value. #### height, width Height and width in pixels to be applied to node before rendering. #### style An object whose properties to be copied to node's style before rendering. You might want to check [this reference](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Properties_Reference) for JavaScript names of CSS properties. #### quality A number between 0 and 1 indicating image quality (e.g. 0.92 => 92%) of the JPEG image. Defaults to 1.0 (100%) #### cacheBust Set to true to append the current time as a query string to URL requests to enable cache busting. Defaults to false #### imagePlaceholder A data URL for a placeholder image substituted when fetching an **image** resource (`ResourceType.IMAGE` / `ResourceType.CSS_IMAGE`) fails. It is **not** applied to fonts or stylesheets — those drop on failure so the CSS fallback (font stack / cascade) applies, rather than forcing an image in as a font. [`requestInterceptor`](#requestinterceptor) will fire first and thus takes precedence over it. Defaults to undefined. #### onImageError A callback invoked whenever an external resource (image, font, etc.) cannot be fetched. It receives an object `{ url, message, status, willUsePlaceholder }` where `willUsePlaceholder` is `true` if a substitute will be used (an `imagePlaceholder`, or a value supplied by [`requestInterceptor`](#requestinterceptor)'s failure call) and `false` if the resource is dropped (resolved to an empty string). This is purely observational — rendering still degrades gracefully — and is useful for logging or telemetry of broken resources. A handler that throws is caught and logged so it can't break the render. Defaults to undefined. Sample use: ```javascript domtoimage.toPng(node, { onImageError: ({ url, status, willUsePlaceholder }) => { console.warn(`dom-to-image: ${url} failed (status ${status})`, { willUsePlaceholder, }); }, }); ``` #### copyDefaultStyles Set to true to enable the copying of the default styles of elements. This will make the process faster. Try disabling it if seeing extra padding and using resetting / normalizing in CSS. Defaults to true. #### disableInlineImages Set to true to disable the normal inlining images into the SVG output. This will generate SVGs that reference the original image files, so they my break if a referenced URL fails. This is always safe to use when generating a PNG/JPG file because the entire SVG image is rendered. #### ensureShown Set to true to force the node you pass in to be rendered even when it is hidden by its own `display: none` or `opacity: 0`. This is **opt-in** and applies only to the captured **root**: deliberate hiding of elements _inside_ the subtree (e.g. a collapsed panel) is left intact. Because a `display: none` element has no layout box, the original is briefly revealed in place to measure it (synchronously, so nothing flashes on screen, though it does force a layout reflow and may notify a `ResizeObserver`/`IntersectionObserver` watching the node) and its live styles are restored immediately afterward. The element's real shown `display` is recovered where possible — if the node is hidden by an inline `style="display:none"`, dropping it lets the cascade restore the true value (e.g. a class's `display: flex`); only when a _stylesheet_ rule hides it is the display approximated by the element's tag default (`block`, `inline`, `table`, …), since the intended value is then unknowable. Note that a `display: none` on an **ancestor** above the captured node is not covered — move the node out or reveal the ancestor yourself. Defaults to `false`. #### styleCaching Selects how computed-style lookups are cached while cloning, as a speed/accuracy trade-off. Accepts `'strict'` (cache keyed on the full tag-ancestry path — most accurate) or `'relaxed'` (cache keyed on only the element and its nearest ascent-stopping ancestor — fewer cache misses, faster). Defaults to `'strict'`. #### disableEmbedFonts Set to true to skip discovering and embedding `@font-face` web fonts into the output. Defaults to false (fonts are embedded). #### ignoreCSSRuleErrors Set to true to suppress the `console.error` that is logged when a stylesheet's CSS rules can't be read while discovering `@font-face` web fonts. This typically happens for cross-origin (CDN) stylesheets, which throw a `SecurityError` on `cssRules` access. The failure is benign — it's already handled gracefully and the capture still succeeds — so this option just quiets the repeated console noise on font-heavy pages. Defaults to false (errors are logged). #### logger A console-like sink (`{ warn?, error? }`) that the library's own diagnostics — failed resource fetches, unreadable cross-origin stylesheets, a throwing `requestInterceptor` / `onImageError` handler, the canvas-size clamp warning, etc. — are routed through. Defaults to a logger that delegates to the global `console`. Provide your own to **redirect** that output to a custom sink or **silence** it. Methods are optional and a missing one drops that level, so `{}` silences everything and `{ error: fn }` keeps only errors: ```javascript // send the library's noise to your telemetry instead of the console domtoimage.toPng(node, { logger: { warn: track.warn, error: track.error } }); // silence it entirely domtoimage.toPng(node, { logger: {} }); ``` This is the broad "shhh / capture everything" knob; the per-message flags (e.g. [ignoreCSSRuleErrors](#ignorecssruleerrors)) still control _whether_ a given message is emitted at all, while `logger` controls _where_ emitted output goes. [onImageError](#onimageerror) remains the structured, observable failure event. #### loadExternalStyleSheet By default a cross-origin stylesheet (e.g. a CDN font stylesheet) exposes no readable `cssRules`, so its `@font-face` web fonts can't be discovered and are left unembedded. Set `loadExternalStyleSheet: true` to **fetch and re-parse** such a stylesheet so its fonts can be embedded. You can also pass a predicate `(href) => boolean` to scope which sheets are fetched: ```javascript domtoimage.toPng(node, { loadExternalStyleSheet: (href) => href.includes('fonts.example.com'), }); ``` It is opt-in (default `false`) because it adds a network fetch per matched stylesheet. The fetch flows through [`requestInterceptor`](#requestinterceptor) (with `type === ResourceType.STYLESHEET`), [`corsImg`](#corsimg), credentials, and the URL cache, so a CORS-blocked stylesheet can be supplied or proxied the same way images and fonts are — and `requestInterceptor` is in fact consulted for **every** external stylesheet, so you can supply one from a cache even with this option off. It degrades quietly: if the fetch is CORS-blocked or fails and nothing supplies it, that sheet is simply skipped (the prior behavior). Relative `url()`s in the fetched CSS are resolved against the stylesheet's own URL. #### httpTimeout Timeout in milliseconds for the XHR requests used to fetch external resources (images, fonts). On timeout the `imagePlaceholder` is used if set, otherwise the request fails. Defaults to 30000 (30 seconds). #### useCredentials Set to true to send authentication credentials (cookies, HTTP auth) with cross-origin (CORS) requests for external resources, i.e. sets `withCredentials` on the XHR and `crossOrigin = 'use-credentials'` on images. Defaults to false. #### useCredentialsFilters An array of patterns; when non-empty, `useCredentials` is enabled automatically only for URLs that match one of the patterns (each is used with `String.prototype.search`). Lets you scope credentialed requests to specific hosts. Defaults to `[]`. #### corsImg Configuration for routing cross-origin image requests through a proxy to work around [CORS](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS) restrictions. An object with `url` (the proxy endpoint, where the token `#{cors}` is replaced by the target URL), optional `method` (`'GET'` or `'POST'`), optional `headers`, and optional `data` (request body, with `#{cors}` substituted in any string values). See [Alternative Solutions to CORS Policy Issue](#alternative-solutions-to-cors-policy-issue) below. Defaults to undefined. #### requestInterceptor A function hook to **supply** or **recover** any external resource (images, `@font-face` fonts, and stylesheets). It is the general primitive behind resource handling. It is called as `requestInterceptor(url, context)`, where `context` carries the resource `type` and a `status` that tells you which of two situations you're in: - **Before the fetch** — `context.status` is `undefined`. Return a `data:` URL string (or a promise of one) to supply the resource yourself and skip the network request, or return `undefined`/`null` to fall through to the normal fetch. - **After a failed fetch** — `context.status` is the HTTP status (`0` for a network error/timeout). Return a value to use as the fallback — this **takes precedence over `imagePlaceholder`** — or return `undefined`/`null` to fall back to `imagePlaceholder` (for image types only) and then to dropping the resource. Note `imagePlaceholder` is applied only for `IMAGE`/`CSS_IMAGE`; a failed `FONT` (or `STYLESHEET`) drops so the CSS fallback applies, so `requestInterceptor` is the way to recover those. > **Test the phase as `status === undefined`, not `!status`** — a network error/timeout > reports `status: 0`, which is falsy. `context.type` is the kind of resource, one of the [`domtoimage.ResourceType`](#resource-types) constants (`IMAGE`, `CSS_IMAGE`, `FONT`, `STYLESHEET`), so you can return a different result per kind. A successful pre-fetch result is cached like a normal fetch, and a handler that throws is caught so it can't break the render. Useful for serving resources from an in-memory/app cache, supplying deterministic fixtures in tests, or implementing a custom resolver/fallback. Defaults to undefined. ```javascript domtoimage.toPng(node, { requestInterceptor: (url, { type, status }) => { if (status === undefined && myCache.has(url)) { return myCache.get(url); // before fetch: a data: URL (or a Promise of one) } if (status !== undefined) { // the fetch failed — supply a kind-appropriate fallback return type === domtoimage.ResourceType.FONT ? myFontFallback : myImageFallback; } return undefined; // fetch normally / fall back to imagePlaceholder }, }); ``` It runs before `corsImg` rewriting and the normal XHR, so a pre-fetch value bypasses both. See [Resource handling: requestInterceptor vs corsImg vs imagePlaceholder](#resource-handling-requestinterceptor-vs-corsimg-vs-imageplaceholder) under _Things to watch out for_ for how these relate. #### Resource types `domtoimage.ResourceType` is a frozen object of the resource kinds passed to [`requestInterceptor`](#requestinterceptor) as `context.type`. Compare against these constants rather than hard-coding the string values: | Constant | Value | Covers | | ------------------------------------ | -------------- | -------------------------------------------------------------------------------------------- | | `domtoimage.ResourceType.IMAGE` | `'image'` | `<img>` and SVG `<image>` content images | | `domtoimage.ResourceType.CSS_IMAGE` | `'css-image'` | any image from a CSS property (`background`, `mask`, `content`, `border-image`, `cursor`, …) | | `domtoimage.ResourceType.FONT` | `'font'` | `@font-face` `src` web fonts | | `domtoimage.ResourceType.STYLESHEET` | `'stylesheet'` | external stylesheets | (The values are plain strings — debuggable and serializable — so they're safe to log or compare directly; the constants just remove the magic-string footgun.) #### scale Scale value to be applied on canvas's `ctx.scale()` on both x and y axis. Can be used to increase the image quality with higher image size. #### pixelRatio Device-pixel-ratio multiplier for the rasterized output (`toPng`, `toJpeg`, `toBlob`, `toCanvas`). Defaults to `1` (CSS pixels, unchanged). Set it to `window.devicePixelRatio` to get crisp output on high-DPI / Retina displays: ```javascript domtoimage.toPng(node, { pixelRatio: window.devicePixelRatio }); ``` It composes with [scale](#scale) (the effective multiplier is `scale × pixelRatio`). If the requested canvas (`width × height × scale × pixelRatio`) would exceed the browser's canvas size limit, the multiplier is clamped to fit and a warning is logged, so a large capture degrades predictably instead of coming out partial or blank (see _Things to watch out for_). #### preserveScroll By default every scrollable element is rendered scrolled to its top/left. Set `preserveScroll: true` to reflect each element's current `scrollLeft`/`scrollTop` instead, so the output matches what's actually scrolled into view: ```javascript domtoimage.toPng(node, { preserveScroll: true }); ``` It's opt-in (default `false`) so existing output is unchanged. Implemented by clipping each scrolled element and translating its content by the scroll offset — which works for normal flow and flex/grid content; deeply transformed or `position: fixed` descendants inside a scroll container are the edge cases. ### Alternative Solutions to CORS Policy Issue Are you facing a [CORS policy](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS) issue in your app? Don't worry, there are alternative solutions to this problem that you can explore. Here are some options to consider: 1. **Use the option.corsImg support by passing images** With this option, you can setup a proxy service that will process the requests in a safe CORS context. 2. **Use third-party services like [allOrigins](https://allorigins.win/).** With this service, you can fetch the source code or an image in base64 format from any website. However, this method can be a bit slow. 3. **Set up your own API service.** Compared to third-party services like [allOrigins](https://allorigins.win/), this method can be faster, but you'll need to convert the image URL to base64 format. You can use the "[image-to-base64](https://github.com/renanbastos93/image-to-base64)" package for this purpose. 4. **Utilize [server-side functions](https://nextjs.org/docs/basic-features/data-fetching/get-server-side-props) features of frameworks like [Next.js](https://nextjs.org/).** This is the easiest and most convenient method, where you can directly fetch a URL source within [server-side functions](https://nextjs.org/docs/basic-features/data-fetching/get-server-side-props) and convert it to base64 format if needed. By exploring these alternative solutions, you can overcome [the CORS policy](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS) issue in your app and ensure that your images are accessible to everyone. ## Browsers It's tested on latest Chrome and Firefox (49 and 45 respectively at the time of writing), with Chrome performing significantly better on big DOM trees, possibly due to it's more performant SVG support, and the fact that it supports `CSSStyleDeclaration.cssText` property. _Internet Explorer is not (and will not be) supported, as it does not support SVG `<foreignObject>` tag_ _Safari [is not supported](https://github.com/tsayen/dom-to-image/issues/27), as it uses a stricter security model on the `<foreignObject>` tag (and has flaky image-decode timing). The suggested workaround is to use `toSvg` and render on the server._ ## Dependencies The newest language features the code relies on are `globalThis` (ES2020) and `Promise.prototype.finally` (ES2018), so it needs at least Chrome 71, Edge 79, Firefox 65, Opera 58, Safari 12.1, or Node 12. ### Source Only standard lib is currently used, but make sure your browser supports: - [Promise](https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/Promise) - SVG `<foreignObject>` tag ### Tests As of this v3 branch chain, the testing jig is taking advantage of the `onclone` hook to insert the clone-output into the testing page. This should make it a tiny bit easier to track down where exactly the inlining of CSS styles against the DOM nodes is wrong. Most importantly, tests **only** depend on: - [ocrad.js](https://github.com/antimatter15/ocrad.js), for the parts when you can't compare images (due to the browser rendering differences) and just have to test whether the text is rendered #### Running the tests | Command | Runs | | --------------------------------------------------- | -------------------------------------------------------------- | | `npm test` | full suite, Chrome (image-comparison + logic) | | `npm run test <pattern>` | only tests whose title matches, e.g. `npm run test border` | | `npm run test:chrome` / `npm run test:firefox` | full suite on a specific browser | | `npm run test:logic` / `npm run test:logic:firefox` | OS-robust logic subset (skips image comparisons; what CI runs) | | `npm run test:node` | Node/SSR smoke test (no browser) | `npm run test <pattern>` filters by Mocha title (describe + it) — pass a group name, several words (`npm run test render web fonts`), or an exact single-test name. Under the hood it sets `GREP`, which you can also use directly to filter any of the other scripts (POSIX shells / WSL): `GREP=border npm run test:firefox`. The suite is grouped so a name targets a slice of it: - **`rendering`** — `content` (`svg`, `images`, `fonts`, `text`), `layout` (`sizing`, `styles`, `visibility`), `api` (`output formats`, `options`, `user input`), `robustness` - **`impl`** — `inliner`, `util`, `fontFaces`, `image loading`, `style keys` So `npm run test fonts`, `npm run test layout`, `npm run test impl`, or `npm run test #205` (issue numbers live in the test titles) all work. Other environment variables compose with any script: - `LOGIC_ONLY=1` — skip the image-comparison tests; `HEADLESS=1` — headless browser; `KARMA_BROWSER=firefox`; `DPR=1.25` — device-pixel-ratio; `UPDATE_CONTROLS=1` — re-bake the reference images (same environment only). > Firefox can't match Chrome-baked control images, so run it with `LOGIC_ONLY=1` (the > `test:logic:firefox` script already does). ## How it works There might some day exist (or maybe already exists?) a simple and standard way of exporting parts of the HTML to image (and then this script can only serve as an evidence of all the hoops I had to jump through in order to get such obvious thing done) but I haven't found one so far. This library uses a feature of SVG that allows having arbitrary HTML content inside of the `<foreignObject>` tag. So, in order to render that DOM node for you, following steps are taken: 1. Clone the original DOM node recursively 1. Compute the style for the node and each sub-node and copy it to corresponding clone - and don't forget to recreate pseudo-elements, as they are not cloned in any way, of course 1. Embed web fonts - find all the `@font-face` declarations that might represent web fonts - parse file URLs, download corresponding files - base64-encode and inline content as `data:` URLs - concatenate all the processed CSS rules and put them into one `<style>` element, then attach it to the clone 1. Embed images - embed image URLs in `<img>` elements - inline images used in `background` CSS property, in a fashion similar to fonts 1. Serialize the cloned node to XML 1. Wrap XML into the `<foreignObject>` tag, then into the SVG, then make it a data URL 1. Optionally, to get PNG content or raw pixel data as a Uint8Array, create an Image element with the SVG as a source, and render it on an off-screen canvas, that you have also created, then read the content from the canvas 1. Done! ### Beyond the basics A few things the cloning step does that aren't obvious from the list above: - **SVG `<use>``<symbol>` inlining.** An `<svg>` often paints an icon with `<use href="#icon">` where the referenced `<symbol>` (or any element) lives **elsewhere on the page** — outside the node you're rendering. That target would never be cloned, so the `<use>` would render nothing. The library detects each `<use>`, resolves its `href`/`xlink:href` against the **live** document, and injects a copy of the referenced element into a hidden `<defs>` in the output so the reference still resolves in the standalone image. The `<use>` element itself is left in place (keeping its own position, size, and inherited `currentColor`). Same-document references only — external sprite files (`sprite.svg#icon`) are left untouched. - **Default-style optimization (`styleCaching`).** Copying every computed style onto every clone produces enormous SVGs. Instead, the library computes each element's _browser default_ styles (for its tag, in a throwaway sandbox iframe) and emits only the properties that actually differ from the default or the parent. `styleCaching` (`'strict'` by default, or `'relaxed'`) tunes how aggressively those per-tag computations are reused across siblings. - **Pseudo-elements and form state.** `::before`/`::after` aren't cloned by the DOM, so they're recreated as real elements carrying the pseudo-element's computed style. Current values of form controls (`<input>`, `<textarea>`, checked/selected state) are copied too, since those live in the DOM, not in attributes. - **Open shadow DOM.** Open shadow roots and their slot-assigned (projected) nodes are walked and flattened into the clone, so web-component content renders. - **Cross-origin resources.** Web fonts and images are fetched and base64-inlined; for images that block CORS you can route them through a proxy with the [corsImg](#corsimg) option, send cookies with `useCredentials`/`useCredentialsFilters`, or cap slow fetches with `httpTimeout`. A broken **content image** degrades gracefully (see _Things to watch out for_ below). - **No mutation of your DOM.** All of this happens on a detached clone, and any temporary helpers (the sandbox iframe, wrapper spans for non-element nodes) are tracked and removed in a `finally`, so a render that throws part-way can't leak nodes into your page. ## Using Typescript This package ships its own type definitions (`dom-to-image-more.d.ts`), so no separate `@types/...` install is needed. Just import and use it: ```typescript import domtoimage, { Options } from 'dom-to-image-more'; const node = document.getElementById('my-node')!; const options: Options = { quality: 0.95, styleCaching: 'relaxed' }; domtoimage.toPng(node, options).then((dataUrl: string) => { /* ... */ }); ``` The bundled types cover every rendering option documented above (including fork-specific ones such as `adjustClonedNode`, `filterStyles`, `styleCaching`, `corsImg`, `useCredentials`/`useCredentialsFilters`, `httpTimeout`, `disableEmbedFonts`, `ignoreCSSRuleErrors`, and `requestInterceptor`), and the `ResourceType` constants and type are exported alongside `Options`. The default import works with `esModuleInterop` enabled; otherwise use `import domtoimage = require('dom-to-image-more');`. The `impl` member is intentionally typed as `unknown`, since it is an internal surface that may change between releases and should not be depended on; cast it explicitly if you need to reach into it for testing. ## Things to watch out for - **Capture a fully-loaded page.** Before rasterizing, the library waits for web fonts the document is already loading (`document.fonts.ready`), so a capture taken while your fonts are mid-download still gets the real glyphs and correct metrics. It **cannot** wait for a stylesheet you have not finished loading, though: if you add a `<link rel="stylesheet">` (e.g. an icon font) and call `toPng`/`toSvg` in the same tick, its `@font-face` rules may not be in the CSSOM yet, so the font won't be found or embedded and the glyph will be missing. Render after the page (and its stylesheets) have loaded — e.g. `await document.fonts.ready` yourself, or wait for the `<link>`'s `load` event, before capturing. - **Server-side rendering (SSR) needs a browser DOM.** The library renders by reading computed styles and rasterizing through the browser, so it cannot run where there is no `document` (Angular Universal, Next.js server, plain Node). It **imports** safely under SSR, but a render call (`toPng`/`toSvg`/…) rejects with a clear `a browser DOM is required (SSR)` error rather than a raw `ReferenceError`. Run the capture only in the browser — e.g. guard with Angular's `isPlatformBrowser`, a `typeof window !== 'undefined'` check, or a dynamic import in a client-only lifecycle hook. (jsdom works if you pass a node that belongs to a jsdom document.) - if the DOM node you want to render includes a `<canvas>` element with something drawn on it, it should be handled fine, unless the canvas is [tainted](https://developer.mozilla.org/en-US/docs/Web/HTML/CORS_enabled_image) - in this case rendering will rather not succeed. - at the time of writing, Firefox has a problem with some external stylesheets (see issue #13). In such case, the error will be caught and logged. - By design failed resources are handled at two different levels. A broken **content image** (an `<img>` inside the node you're rendering) degrades gracefully — it's skipped and the rest of the node still renders, and you can observe it via the [onImageError](#onimageerror) callback. But a failure of the **final rasterization** (turning the SVG into a PNG/JPEG/canvas) or a `<canvas>` snapshot is fatal — the returned promise rejects so your `.catch()` can handle it, rather than silently returning a blank image. In short: missing content degrades, a broken output rejects. - A node hidden by an ancestor's `visibility: hidden` **is** rendered: because you asked to capture that node explicitly, the captured root is forced visible (and its descendants follow), while any deliberate per-element `visibility: hidden` inside the subtree is still honored. Two related cases — a `display: none` or `opacity: 0` on the node you pass — are **not** rescued by default, because they are not inherited and the value is often intentional. Opt in with [ensureShown](#ensureshown) to render those, or un-hide the node yourself first (note that a `display: none` **ancestor** above the captured node is not covered by `ensureShown` — move the node out or reveal the ancestor). - **High-DPI / Retina output looks soft, and very large captures can come out partial.** By default the output is rasterized at CSS-pixel resolution (1×). On high-DPI displays that can look soft when shown at native resolution — pass [pixelRatio](#pixelratio): `window.devicePixelRatio` for a crisp capture. Browsers also cap canvas size; a capture whose `width × height × scale × pixelRatio` exceeds that cap would otherwise yield a **partial or blank** bitmap, so the library clamps the multiplier to fit and logs a warning instead (render a smaller region, or lower `scale`/`pixelRatio`, if you hit it). - **At a fractional display scale or browser zoom (e.g. Windows 125%, or 125% page zoom), flex/grid layouts may be shifted by ~1px in the output.** The capture is rasterized from an SVG `<foreignObject>` that re-lays-out the content at 100% zoom / whole CSS pixels, so sub-pixel positions snapped by the live render at a fractional device-pixel-ratio can land on slightly different boundaries. This is an inherent limitation of the approach, not a flex bug (the styles are reproduced faithfully); `pixelRatio: window.devicePixelRatio` can reduce it but won't fully eliminate it. - **`backdrop-filter` is not rendered.** The property is copied to the clone faithfully, but a `backdrop-filter` blurs/tints whatever is painted _behind_ an element, and the captured node is rasterized inside an isolated SVG `<foreignObject>` with nothing behind it to sample — so there's nothing for the filter to act on and it has no visible effect. This is structural to the SVG-foreignObject technique. (A regular `filter` on the element itself works fine; it's specifically the _backdrop_ variant that can't be reproduced.) - **A WebGL `<canvas>` can capture blank.** A regular 2D canvas is snapshotted fine, but a WebGL context only retains its drawing buffer for reading if it was created with `preserveDrawingBuffer: true` — otherwise the browser clears it after compositing and the snapshot comes out empty. This is a caller-side WebGL setting; set it when you create the context if you need the canvas (or any library that draws into one, e.g. some map/chart renderers) to be captured. Cannot be worked around from this library. - **`<video>` frames and cross-origin content can't be captured.** A `<video>` element rasterizes to nothing (and DRM/cross-origin video would taint the canvas anyway); cross-origin `<iframe>` content is inaccessible to the page, so it can't be cloned. For a video, capture a poster image or draw the current frame to a same-origin `<canvas>` yourself and render that instead. - **Only what's actually in the DOM is captured.** Virtualized / windowed lists, lazy-loaded images that haven't loaded, and other content rendered on-demand are not in the live DOM at capture time, so they won't appear in the output. Fully expand/scroll the content into the DOM (and let images finish loading) before rendering. - **Safari is unreliable.** Safari applies a stricter security model to SVG `<foreignObject>` and has flaky image-decode timing, so captures may come out blank or vary run-to-run. It is not a supported target; if you need it, prefer [toSvg](#usage) and rasterize server-side. See the [Browsers](#browsers) note. ### Resource handling: requestInterceptor vs corsImg vs imagePlaceholder These three options all touch external-resource fetching, and it's reasonable to ask why all three exist. They operate at different points and compose, rather than overlapping: - [`requestInterceptor`](#requestinterceptor) is the **general primitive**: a single function that can _supply_ a resource before the fetch (when `status === undefined`) or _recover_ one after any failed fetch (when `status` is numeric). Use it when you want full programmatic control — a cache, a custom resolver, or a computed fallback. - [`corsImg`](#corsimg) is a **declarative convenience for one specific case**: routing cross-origin images through a proxy. It still performs the real XHR (with the URL, method, headers, and body you configure) rather than returning bytes, so it's not expressible as a plain pre-fetch return. `requestInterceptor` runs first; if it returns a value, `corsImg` is bypassed for that URL. - [`imagePlaceholder`](#imageplaceholder) is a **declarative convenience for the failure case**: a single static `data:` URL substituted when an **image** fetch fails (`IMAGE`/`CSS_IMAGE` only — a failed font/stylesheet drops so the CSS fallback applies). It is the static-value shorthand for what `requestInterceptor`'s failure call does programmatically, and the interceptor takes precedence when both are set. A fetch "fails"and so triggers the recovery call / `imagePlaceholder`not only on a network error, timeout, or non-2xx status, but also when a response comes back that isn't a usable image/font (an empty or non-`Blob` body, or one that can't be decoded). In that last case `status` is whatever the server returned (possibly a `2xx`), so treat **any** failure call (`status !== undefined`) as "the resource couldn't be produced," rather than inferring success from a `2xx`. In short: reach for `corsImg`/`imagePlaceholder` for the common proxy/placeholder cases, and `requestInterceptor` when you need a programmatic hook for either supplying or recovering resources. Effective order for any one URL is **`requestInterceptor`(pre-fetch) → `corsImg` rewrite → fetch → `requestInterceptor`(failure) → `imagePlaceholder` (images only) → drop**, with [`onImageError`](#onimageerror) observing any failure along the way. ## Authors Marc Brooks, Anatolii Saienko (original dom-to-image), Paul Bakaus (original idea), Aidas Klimas (fixes), Edgardo Di Gesto (fixes), 樊冬 Fan Dong (fixes), Shrijan Tripathi (docs), SNDST00M (optimize), Joseph White (performance CSS), Phani Rithvij (test), David DOLCIMASCOLO (packaging), Zee (ZM) @zm-cttae (many major updates), Joshua Walsh @JoshuaWalsh (Firefox issues), Emre Coban @emrecoban (documentation), Nate Stuyvesant @nstuyvesant (fixes), King Wang @eachmawzw (CORS image proxy), TMM Schmit @tmmschmit (useCredentialsFilters), Aravind @codesculpture (fix overridden props), Shi Wenyu @cWenyu (shadow slot fix), David Burns @davidburns573 and Yujia Cheng @YujiaCheng1996 (font copy optional), Julien Dorra @juliendorra (documentation), Sean Zhang @SeanZhang-eaton (regex fixes), Ludovic Bouges @ludovic (style property filter), Roland Ma @RolandMa1986 (URL regex)", Kasim Tan @kasimtan, Matthias Zach @matthiaszach (iframe fixes), Kamran Ayub @kamranayub (filter URL option), Liu YuanYuan @mgenware, Davey Tran @DaveyTran, Nathan Fiscus @NathanFiscus (requestInterceptor), TechValidate @TechValidate (pseudo-element filter), Sizle @SizlePtyLtd, kbasten @kbasten, and Michal Bryxí @MichalBryxi (external stylesheet loading) ## License MIT