├── .vscode └── settings.json ├── types └── index.d.ts ├── vite.config.js ├── tsconfig.json ├── package.json ├── LICENSE ├── .gitignore ├── index.html ├── dist ├── html-element-to-image.js ├── html-element-to-image.umd.js └── html-element-to-image.mjs ├── README.md └── src └── index.ts /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.tabSize": 4 3 | } -------------------------------------------------------------------------------- /types/index.d.ts: -------------------------------------------------------------------------------- 1 | export interface CaptureOptions { 2 | targetNode: HTMLElement | Element; 3 | excludedNodes: HTMLElement[] | Element[]; 4 | customStyle: string; 5 | returnType: string; 6 | } 7 | 8 | export default function nodeToDataURL(userConfig: Partial): Promise; 9 | -------------------------------------------------------------------------------- /vite.config.js: -------------------------------------------------------------------------------- 1 | // vite.config.js 2 | import { resolve } from 'path' 3 | import { defineConfig } from 'vite' 4 | 5 | export default defineConfig({ 6 | build: { 7 | lib: { 8 | formats: ['cjs', 'es', 'umd'], 9 | entry: resolve(__dirname, 'src/index.ts'), 10 | name: 'nodeToDataURL', 11 | fileName: 'html-element-to-image', 12 | } 13 | }, 14 | }) -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "module": "ESNext", 5 | "outDir": "./dist", 6 | "strict": true, 7 | "esModuleInterop": true, 8 | "lib": [ 9 | "ESNext", 10 | "dom" 11 | ], 12 | "declaration": true, 13 | "declarationDir": "types", 14 | "typeRoots": ["types"] 15 | } 16 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "html-element-to-image", 3 | "version": "3.0.0", 4 | "description": "Capture a image from any given HTML element, fast.", 5 | "repository": "https://github.com/Hammster/html-element-to-image", 6 | "author": "Hans Koch ", 7 | "license": "MIT", 8 | "types": "./types/index.d.ts", 9 | "scripts": { 10 | "build": "vite build", 11 | "dev": "vite" 12 | }, 13 | "devDependencies": { 14 | "vite": "^5.2.12", 15 | "typescript": "^5.4.5" 16 | }, 17 | "main": "./dist/html-element-to-image.js", 18 | "module": "./dist/html-element-to-image.mjs", 19 | "exports": { 20 | ".": { 21 | "import": "./dist/html-element-to-image.mjs", 22 | "require": "./dist/html-element-to-image.js" 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Hans Koch 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (https://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # TypeScript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | 60 | # next.js build output 61 | .next 62 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Document 8 | 35 | 36 | 37 | Source: 38 |
39 |
40 | T説🧜pѬѬasѪ"§§)("!编/)$=?!°&%)?§"$(§sw汉字编码§"$(§sw汉字方法orФdpѬѬasѪ"§§)("!/)$=?!°&%)?编码方法orФd 41 |
42 |
43 |
44 | 45 | Target (Copy without child and overloaded style): 46 |
47 | 48 |
49 | 65 | 66 | -------------------------------------------------------------------------------- /dist/html-element-to-image.js: -------------------------------------------------------------------------------- 1 | "use strict";const a="___",l=`${a}capture-show`,m=`${a}capture-hide`,u=`${a}force-overflow`,h={customStyle:"",excludedNodes:[],returnType:"dataUrl",targetNode:document.body};let c=h;function p(n){n.classList.add(l);let t=n.parentElement;for(const e of c.excludedNodes)e.classList.add(m);for(;t;)t.classList.add(u),t=t.parentElement}function x(n){n.classList.remove(l);let t=n.parentElement;for(const e of c.excludedNodes)e.classList.remove(m);for(;t;)t.classList.remove(u),t=t.parentElement}function f(n){const t=document.head.cloneNode(!0),e=document.createElement("style");for(const o of Array.from(t.childNodes))o instanceof HTMLStyleElement||o.remove();e.innerText+="foreignObject>div {",e.innerText+="position: fixed;",e.innerText+=`top: ${window.outerHeight}px;`,e.innerText+="overflow: visible;",e.innerText+="}",e.innerText+=`.${l} {`,e.innerText+="position: fixed;",e.innerText+="left: 0;",e.innerText+="top: 0;",e.innerText+=`width: ${n.width}px;`,e.innerText+=`height: ${n.height}px;`,e.innerText+="}",e.innerText+=`.${m} {display: none !important;}`,e.innerText+=`.${u} {overflow: visible !important;}`,e.innerText+=c.customStyle,t.appendChild(e);let i="";for(const o of Array.from(t.querySelectorAll("style")))i+=o.outerHTML;return i}function v(){return document.body.innerHTML.replace(/)<[^<]*)*<\/script>/g,"")}function T(n,t){p(n);const e=f(t),i=v();-t.left,-t.top;const o=document.createElementNS("http://www.w3.org/2000/svg","svg");o.setAttribute("xmlns","http://www.w3.org/2000/svg"),o.setAttribute("height",t.height.toString()),o.setAttribute("width",t.width.toString());const s=document.createElementNS("http://www.w3.org/2000/svg","foreignObject");s.removeAttribute("xmlns"),s.setAttribute("x","0"),s.setAttribute("y","0"),s.setAttribute("height",window.outerHeight.toString()),s.setAttribute("width",window.outerWidth.toString());const r=document.createElement("div");return r.setAttribute("xmlns","http://www.w3.org/1999/xhtml"),r.setAttribute("xmlns:xlink","http://www.w3.org/1999/xlink"),r.setAttribute("class",document.body.className),r.innerHTML=i,s.appendChild(r),o.insertAdjacentHTML("afterbegin",e),o.appendChild(s),x(n),new XMLSerializer().serializeToString(o)}function b(n){c={...h,...n};const t=c.targetNode,e=t.getBoundingClientRect(),i=T(t,e),s=`data:image/svg+xml,${encodeURIComponent(i)}`,r=document.createElement("canvas");return new Promise((g,w)=>{if(!r){w();return}const d=document.createElement("img");d.addEventListener("error",()=>{r.remove(),w()}),d.addEventListener("load",()=>{r.width=e.width,r.height=e.height,r.getContext("2d").drawImage(d,0,0),g(r.toDataURL()),r.remove()}),d.src=s})}module.exports=b; 2 | -------------------------------------------------------------------------------- /dist/html-element-to-image.umd.js: -------------------------------------------------------------------------------- 1 | (function(s,d){typeof exports=="object"&&typeof module<"u"?module.exports=d():typeof define=="function"&&define.amd?define(d):(s=typeof globalThis<"u"?globalThis:s||self,s.nodeToDataURL=d())})(this,function(){"use strict";const s="___",d=`${s}capture-show`,u=`${s}capture-hide`,m=`${s}force-overflow`,f={customStyle:"",excludedNodes:[],returnType:"dataUrl",targetNode:document.body};let a=f;function g(n){n.classList.add(d);let t=n.parentElement;for(const e of a.excludedNodes)e.classList.add(u);for(;t;)t.classList.add(m),t=t.parentElement}function w(n){n.classList.remove(d);let t=n.parentElement;for(const e of a.excludedNodes)e.classList.remove(u);for(;t;)t.classList.remove(m),t=t.parentElement}function x(n){const t=document.head.cloneNode(!0),e=document.createElement("style");for(const r of Array.from(t.childNodes))r instanceof HTMLStyleElement||r.remove();e.innerText+="foreignObject>div {",e.innerText+="position: fixed;",e.innerText+=`top: ${window.outerHeight}px;`,e.innerText+="overflow: visible;",e.innerText+="}",e.innerText+=`.${d} {`,e.innerText+="position: fixed;",e.innerText+="left: 0;",e.innerText+="top: 0;",e.innerText+=`width: ${n.width}px;`,e.innerText+=`height: ${n.height}px;`,e.innerText+="}",e.innerText+=`.${u} {display: none !important;}`,e.innerText+=`.${m} {overflow: visible !important;}`,e.innerText+=a.customStyle,t.appendChild(e);let c="";for(const r of Array.from(t.querySelectorAll("style")))c+=r.outerHTML;return c}function v(){return document.body.innerHTML.replace(/)<[^<]*)*<\/script>/g,"")}function T(n,t){g(n);const e=x(t),c=v();-t.left,-t.top;const r=document.createElementNS("http://www.w3.org/2000/svg","svg");r.setAttribute("xmlns","http://www.w3.org/2000/svg"),r.setAttribute("height",t.height.toString()),r.setAttribute("width",t.width.toString());const i=document.createElementNS("http://www.w3.org/2000/svg","foreignObject");i.removeAttribute("xmlns"),i.setAttribute("x","0"),i.setAttribute("y","0"),i.setAttribute("height",window.outerHeight.toString()),i.setAttribute("width",window.outerWidth.toString());const o=document.createElement("div");return o.setAttribute("xmlns","http://www.w3.org/1999/xhtml"),o.setAttribute("xmlns:xlink","http://www.w3.org/1999/xlink"),o.setAttribute("class",document.body.className),o.innerHTML=c,i.appendChild(o),r.insertAdjacentHTML("afterbegin",e),r.appendChild(i),w(n),new XMLSerializer().serializeToString(r)}function b(n){a={...f,...n};const t=a.targetNode,e=t.getBoundingClientRect(),c=T(t,e),i=`data:image/svg+xml,${encodeURIComponent(c)}`,o=document.createElement("canvas");return new Promise((h,p)=>{if(!o){p();return}const l=document.createElement("img");l.addEventListener("error",()=>{o.remove(),p()}),l.addEventListener("load",()=>{o.width=e.width,o.height=e.height,o.getContext("2d").drawImage(l,0,0),h(o.toDataURL()),o.remove()}),l.src=i})}return b}); 2 | -------------------------------------------------------------------------------- /dist/html-element-to-image.mjs: -------------------------------------------------------------------------------- 1 | const a = "___", l = `${a}capture-show`, m = `${a}capture-hide`, u = `${a}force-overflow`, h = { 2 | customStyle: "", 3 | excludedNodes: [], 4 | returnType: "dataUrl", 5 | targetNode: document.body 6 | }; 7 | let c = h; 8 | function p(n) { 9 | n.classList.add(l); 10 | let t = n.parentElement; 11 | for (const e of c.excludedNodes) 12 | e.classList.add(m); 13 | for (; t; ) 14 | t.classList.add(u), t = t.parentElement; 15 | } 16 | function x(n) { 17 | n.classList.remove(l); 18 | let t = n.parentElement; 19 | for (const e of c.excludedNodes) 20 | e.classList.remove(m); 21 | for (; t; ) 22 | t.classList.remove(u), t = t.parentElement; 23 | } 24 | function f(n) { 25 | const t = document.head.cloneNode(!0), e = document.createElement("style"); 26 | for (const r of Array.from(t.childNodes)) 27 | r instanceof HTMLStyleElement || r.remove(); 28 | e.innerText += "foreignObject>div {", e.innerText += "position: fixed;", e.innerText += `top: ${window.outerHeight}px;`, e.innerText += "overflow: visible;", e.innerText += "}", e.innerText += `.${l} {`, e.innerText += "position: fixed;", e.innerText += "left: 0;", e.innerText += "top: 0;", e.innerText += `width: ${n.width}px;`, e.innerText += `height: ${n.height}px;`, e.innerText += "}", e.innerText += `.${m} {display: none !important;}`, e.innerText += `.${u} {overflow: visible !important;}`, e.innerText += c.customStyle, t.appendChild(e); 29 | let i = ""; 30 | for (const r of Array.from(t.querySelectorAll("style"))) 31 | i += r.outerHTML; 32 | return i; 33 | } 34 | function v() { 35 | return document.body.innerHTML.replace(/)<[^<]*)*<\/script>/g, ""); 36 | } 37 | function T(n, t) { 38 | p(n); 39 | const e = f(t), i = v(); 40 | -t.left, -t.top; 41 | const r = document.createElementNS("http://www.w3.org/2000/svg", "svg"); 42 | r.setAttribute("xmlns", "http://www.w3.org/2000/svg"), r.setAttribute("height", t.height.toString()), r.setAttribute("width", t.width.toString()); 43 | const s = document.createElementNS("http://www.w3.org/2000/svg", "foreignObject"); 44 | s.removeAttribute("xmlns"), s.setAttribute("x", "0"), s.setAttribute("y", "0"), s.setAttribute("height", window.outerHeight.toString()), s.setAttribute("width", window.outerWidth.toString()); 45 | const o = document.createElement("div"); 46 | return o.setAttribute("xmlns", "http://www.w3.org/1999/xhtml"), o.setAttribute("xmlns:xlink", "http://www.w3.org/1999/xlink"), o.setAttribute("class", document.body.className), o.innerHTML = i, s.appendChild(o), r.insertAdjacentHTML("afterbegin", e), r.appendChild(s), x(n), new XMLSerializer().serializeToString(r); 47 | } 48 | function y(n) { 49 | c = { ...h, ...n }; 50 | const t = c.targetNode, e = t.getBoundingClientRect(), i = T(t, e), s = `data:image/svg+xml,${encodeURIComponent(i)}`, o = document.createElement("canvas"); 51 | return new Promise((g, w) => { 52 | if (!o) { 53 | w(); 54 | return; 55 | } 56 | const d = document.createElement("img"); 57 | d.addEventListener("error", () => { 58 | o.remove(), w(); 59 | }), d.addEventListener("load", () => { 60 | o.width = e.width, o.height = e.height, o.getContext("2d").drawImage(d, 0, 0), g(o.toDataURL()), o.remove(); 61 | }), d.src = s; 62 | }); 63 | } 64 | export { 65 | y as default 66 | }; 67 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # html-element-to-image 2 | 📷 Capture an image of any given HTML element. 3 | 4 | ## Install 5 | 6 | ```bash 7 | npm install html-element-to-image 8 | ``` 9 | 10 | ## 📖 Example 11 | 12 | Codepen: https://codepen.io/Hammster/pen/ZEYvxLa 13 | 14 | ESM 15 | ```html 16 | 32 | ``` 33 | 34 | CJS 35 | ``` 36 | const nodeToDataURL = require('html-element-to-image'); 37 | ``` 38 | 39 | UMD 40 | > Exposes the function `nodeToDataURL` to the window/global 41 | 42 | ## 🔬 Differences to Other Libraries 43 | 44 | Compared to [html-to-image](https://github.com/bubkoo/html-to-image), [dom-to-image](https://github.com/tsayen/dom-to-image) and [dom-to-image-more](https://github.com/1904labs/dom-to-image-more) this library uses a different approach, which leads to a big performance increase. Instead of making a [deep copy](https://en.wikipedia.org/wiki/Object_copying#Deep_copy) of the targeted element with applied styles via `el.getComputedStyle()`. This library uses the available DOM for serialization. Before and after the serialization, obfuscated classes get added to hide the parts we don't want to be displayed. 45 | 46 | Another difference is that this library has no issue at all with `SVG Sprite Sheets` and `ShadowDOM` since we use the given DOM already. 47 | 48 | 57 | 58 | ## 🛡️ Limitation 59 | 60 | - If the DOM node you want to render includes a `` 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. 61 | - Rendering will failed on huge DOM due to the dataURI [limit variations](https://stackoverflow.com/questions/695151/data-protocol-url-size-limitations/41755526#41755526) in browser implementations. 62 | - Transforms may be affected. Elements that depend on `transform-style: preserve-3d;` do not work in a `foreignObject`, everything get flattened. And this is sadly a requirement for creating a image buffer, and also effects [html-to-image](https://github.com/bubkoo/html-to-image), [dom-to-image](https://github.com/tsayen/dom-to-image) and [dom-to-image-more](https://github.com/1904labs/dom-to-image-more) 63 | - this can be avoided partially by adding a fallback z-index 64 | 65 | ## 💔 Know Issues 66 | 67 | - The canvas is not yet DPI aware 68 | - styles from `html` and `body` can sometimes be overwritten by the [Specificity](https://developer.mozilla.org/en-US/docs/Web/CSS/Specificity) 69 | - Lazy loaded components can break rendering 70 | - Shadow DOM 71 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import type { CaptureOptions } from '../types/index' 2 | 3 | const prefix = '___' 4 | const captureShowClass = `${prefix}capture-show` 5 | const captureHideClass = `${prefix}capture-hide` 6 | const forceOverflowClass = `${prefix}force-overflow` 7 | 8 | const defaultOptions: CaptureOptions = { 9 | customStyle: '', 10 | excludedNodes: [], 11 | returnType: 'dataUrl', 12 | targetNode: document.body 13 | } 14 | 15 | let config: CaptureOptions = defaultOptions 16 | 17 | function addClasses (node: Element): void { 18 | node.classList.add(captureShowClass) 19 | let nodeParent = node.parentElement 20 | 21 | for (const excludedEl of config.excludedNodes) { 22 | excludedEl.classList.add(captureHideClass) 23 | } 24 | 25 | while (nodeParent) { 26 | nodeParent.classList.add(forceOverflowClass) 27 | nodeParent = nodeParent.parentElement 28 | } 29 | } 30 | 31 | function removeClasses (node: Element): void { 32 | node.classList.remove(captureShowClass) 33 | let nodeParent = node.parentElement 34 | 35 | for (const excludedEl of config.excludedNodes) { 36 | excludedEl.classList.remove(captureHideClass) 37 | } 38 | 39 | while (nodeParent) { 40 | nodeParent.classList.remove(forceOverflowClass) 41 | nodeParent = nodeParent.parentElement 42 | } 43 | } 44 | 45 | function serializeHead (elBoundingClientRect: DOMRect): string { 46 | const headClone = document.head.cloneNode(true) as HTMLHeadElement 47 | const captureStyle = document.createElement('style') 48 | 49 | // Only the styled from the head are releavant so we grab them 50 | for (const childElement of Array.from(headClone.childNodes)) { 51 | if (!(childElement instanceof HTMLStyleElement)) { 52 | childElement.remove() 53 | } 54 | } 55 | 56 | // These are the style we apply on the element to move the body out off the visible area. 57 | // The remaining element will be moved back to its original position so it can be rendere without any background. 58 | // Addition Hint: Lineabreaks are parsed as
by the XMLParser so i need to have that syle in one line 59 | captureStyle.innerText += `foreignObject>div {` 60 | captureStyle.innerText += `position: fixed;` 61 | captureStyle.innerText += `top: ${window.outerHeight}px;` 62 | captureStyle.innerText += `overflow: visible;` 63 | captureStyle.innerText += `}` 64 | 65 | captureStyle.innerText += `.${captureShowClass} {` 66 | captureStyle.innerText += `position: fixed;` 67 | captureStyle.innerText += `left: 0;` 68 | captureStyle.innerText += `top: 0;` 69 | captureStyle.innerText += `width: ${elBoundingClientRect.width}px;` 70 | captureStyle.innerText += `height: ${elBoundingClientRect.height}px;` 71 | captureStyle.innerText += `}` 72 | 73 | captureStyle.innerText += `.${captureHideClass} {display: none !important;}` 74 | captureStyle.innerText += `.${forceOverflowClass} {overflow: visible !important;}` 75 | 76 | captureStyle.innerText += config.customStyle 77 | headClone.appendChild(captureStyle) 78 | 79 | let resultHtml = '' 80 | 81 | for (const styleElement of Array.from(headClone.querySelectorAll('style'))) { 82 | resultHtml += styleElement.outerHTML 83 | } 84 | 85 | return resultHtml 86 | } 87 | 88 | function serializeBody (): string { 89 | return ( 90 | document.body.innerHTML.replace(/)<[^<]*)*<\/script>/g, '') 91 | ) 92 | } 93 | 94 | function combineToSvg (node: Element, elBoundingClientRect: DOMRect): string { 95 | // Add Temporary identification classes 96 | addClasses(node) 97 | 98 | const documentHead = serializeHead(elBoundingClientRect) 99 | const documentBody = serializeBody() 100 | 101 | const offesetX = -elBoundingClientRect.left 102 | const offesetY = -elBoundingClientRect.top 103 | 104 | const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg') 105 | svg.setAttribute('xmlns', 'http://www.w3.org/2000/svg') 106 | svg.setAttribute('height', elBoundingClientRect.height.toString()) 107 | svg.setAttribute('width', elBoundingClientRect.width.toString()) 108 | 109 | const foreignObject = document.createElementNS('http://www.w3.org/2000/svg', 'foreignObject') 110 | foreignObject.removeAttribute('xmlns') 111 | foreignObject.setAttribute('x', '0') // offesetX.toString()) 112 | foreignObject.setAttribute('y', '0') // offesetY.toString()) 113 | foreignObject.setAttribute('height', window.outerHeight.toString()) 114 | foreignObject.setAttribute('width', window.outerWidth.toString()) 115 | 116 | const innerHtml = document.createElement('div') 117 | innerHtml.setAttribute('xmlns', 'http://www.w3.org/1999/xhtml') 118 | innerHtml.setAttribute('xmlns:xlink', 'http://www.w3.org/1999/xlink') 119 | innerHtml.setAttribute('class', document.body.className) 120 | 121 | // additional namespace for rendering 122 | innerHtml.innerHTML = documentBody 123 | 124 | foreignObject.appendChild(innerHtml) 125 | 126 | svg.insertAdjacentHTML('afterbegin', documentHead) 127 | svg.appendChild(foreignObject) 128 | 129 | // Remove Temporary identification classes 130 | removeClasses(node) 131 | 132 | const svgString = new XMLSerializer().serializeToString(svg) 133 | return svgString 134 | } 135 | 136 | export default function nodeToDataURL (userConfig: Partial): Promise { 137 | config = { ...defaultOptions, ...userConfig } 138 | 139 | const node = config.targetNode 140 | const elBoundingClientRect = node.getBoundingClientRect() 141 | const svg = combineToSvg(node, elBoundingClientRect) 142 | 143 | // Build base64 data url 144 | const svgBase64 = encodeURIComponent(svg) 145 | const dataURL = `data:image/svg+xml,${svgBase64}` 146 | 147 | const canvas = document.createElement('canvas') 148 | 149 | return new Promise((resolve, reject) => { 150 | if (!canvas) { 151 | reject() 152 | return 153 | } 154 | 155 | // Load data URL into a Image 156 | const img = document.createElement('img') 157 | 158 | // In case the generated dataURL in invalid 159 | img.addEventListener('error', () => { 160 | canvas.remove() 161 | reject() 162 | return 163 | }) 164 | 165 | // Wait for the image to be loaded, otherwise the buffer is empty 166 | img.addEventListener('load', () => { 167 | // For debugging feel free to use this fiddle 168 | // https://jsfiddle.net/j0asnubp/9/ 169 | // just paste in the base64URL 170 | canvas.width = elBoundingClientRect.width 171 | canvas.height = elBoundingClientRect.height 172 | 173 | const ctx = canvas.getContext('2d')! 174 | ctx.drawImage(img, 0, 0) 175 | 176 | resolve(canvas.toDataURL()) 177 | canvas.remove() 178 | }) 179 | 180 | // only attach the data URL after all eventHandler are attached 181 | img.src = dataURL 182 | }) 183 | } 184 | --------------------------------------------------------------------------------