UNPKG

dom-to-image-more

Version:

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

1,297 lines (1,124 loc) 59.1 kB
(function (global) { 'use strict'; const util = newUtil(); const inliner = newInliner(); const fontFaces = newFontFaces(); const images = newImages(); // Default impl options const defaultOptions = { // Default is to copy default styles of elements copyDefaultStyles: true, // Default is to fail on error, no placeholder imagePlaceholder: undefined, // Default cache bust is false, it will use the cache cacheBust: false, // Use (existing) authentication credentials for external URIs (CORS requests) useCredentials: false, // Use (existing) authentication credentials for external URIs (CORS requests) on some filtered requests only useCredentialsFilters: [], // Default resolve timeout httpTimeout: 30000, // Style computation cache tag rules (options are strict, relaxed) styleCaching: 'strict', // Default cors config is to request the image address directly corsImg: undefined, // Callback for adjustClonedNode eventing (to allow adjusting clone's properties) adjustClonedNode: undefined, // Callback to filter style properties to be included in the output filterStyles: undefined, }; const domtoimage = { toSvg: toSvg, toPng: toPng, toJpeg: toJpeg, toBlob: toBlob, toPixelData: toPixelData, toCanvas: toCanvas, impl: { fontFaces: fontFaces, images: images, util: util, inliner: inliner, urlCache: [], options: {}, copyOptions: copyOptions }, }; if (typeof exports === 'object' && typeof module === 'object') { module.exports = domtoimage; // eslint-disable-line no-undef } else { global.domtoimage = domtoimage; } // support node and browsers const ELEMENT_NODE = (typeof Node !== 'undefined' ? Node.ELEMENT_NODE : undefined) || 1; const getComputedStyle = (typeof global !== 'undefined' ? global.getComputedStyle : undefined) || (typeof window !== 'undefined' ? window.getComputedStyle : undefined) || globalThis.getComputedStyle; const atob = (typeof global !== 'undefined' ? global.atob : undefined) || (typeof window !== 'undefined' ? window.atob : undefined) || globalThis.atob; /** * @param {Node} node - The DOM Node object to render * @param {Object} options - Rendering options * @param {Function} options.filter - 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. * @param {Function} options.onclone - Callback function which is called when the Document has been cloned for * rendering, can be used to modify the contents that will be rendered without affecting the original * source document. * @param {String} options.bgcolor - color for the background, any valid CSS color value. * @param {Number} options.width - width to be applied to node before rendering. * @param {Number} options.height - height to be applied to node before rendering. * @param {Object} options.style - an object whose properties to be copied to node's style before rendering. * @param {Number} options.quality - a Number between 0 and 1 indicating image quality (applicable to JPEG only), defaults to 1.0. * @param {Number} options.scale - a Number multiplier to scale up the canvas before rendering to reduce fuzzy images, defaults to 1.0. * @param {String} options.imagePlaceholder - dataURL to use as a placeholder for failed images, default behaviour is to fail fast on images we can't fetch * @param {Boolean} options.cacheBust - set to true to cache bust by appending the time to the request url * @param {String} options.styleCaching - set to 'strict', 'relaxed' to select style caching rules * @param {Boolean} options.copyDefaultStyles - set to false to disable use of default styles of elements * @param {Boolean} options.disableEmbedFonts - set to true to disable font embedding into the SVG output. * @param {Boolean} options.disableInlineImages - set to true to disable inlining images into the SVG output. * @param {Object} options.corsImg - When the image is restricted by the server from cross-domain requests, the proxy address is passed in to get the image * - @param {String} url - eg: https://cors-anywhere.herokuapp.com/ * - @param {Enumerator} method - get, post * - @param {Object} headers - eg: { "Content-Type", "application/json;charset=UTF-8" } * - @param {Object} data - post payload * @param {Function} options.adjustClonedNode - callback for adjustClonedNode eventing (to allow adjusting clone's properties) * @param {Function} options.filterStyles - Should return true if passed propertyName should be included in the output * @return {Promise} - A promise that is fulfilled with a SVG image data URL * */ function toSvg(node, options) { const ownerWindow = domtoimage.impl.util.getWindow(node); options = options || {}; domtoimage.impl.copyOptions(options); const restorations = []; return Promise.resolve(node) .then(ensureElement) .then(function (clonee) { return cloneNode(clonee, options, null, ownerWindow); }) .then(options.disableEmbedFonts ? Promise.resolve(node) : embedFonts) .then(options.disableInlineImages ? Promise.resolve(node) : inlineImages) .then(applyOptions) .then(makeSvgDataUri) .then(restoreWrappers) .then(clearCache); function ensureElement(node) { if (node.nodeType === ELEMENT_NODE) return node; const originalChild = node; const originalParent = node.parentNode; const wrappingSpan = document.createElement('span'); originalParent.replaceChild(wrappingSpan, originalChild); wrappingSpan.append(node); restorations.push({ parent: originalParent, child: originalChild, wrapper: wrappingSpan, }); return wrappingSpan; } function restoreWrappers(result) { // put the original children back where the wrappers were inserted while (restorations.length > 0) { const restoration = restorations.pop(); restoration.parent.replaceChild(restoration.child, restoration.wrapper); } return result; } function clearCache(result) { domtoimage.impl.urlCache = []; removeSandbox(); return result; } function applyOptions(clone) { if (options.bgcolor) { clone.style.backgroundColor = options.bgcolor; } if (options.width) { clone.style.width = `${options.width}px`; } if (options.height) { clone.style.height = `${options.height}px`; } if (options.style) { Object.keys(options.style).forEach(function (property) { clone.style[property] = options.style[property]; }); } let onCloneResult = null; if (typeof options.onclone === 'function') { onCloneResult = options.onclone(clone); } return Promise.resolve(onCloneResult).then(function () { return clone; }); } function makeSvgDataUri(clone) { const width = options.width || util.width(node); const height = options.height || util.height(node); return Promise.resolve(clone) .then(function (svg) { svg.setAttribute('xmlns', 'http://www.w3.org/1999/xhtml'); return new XMLSerializer().serializeToString(svg); }) .then(util.escapeXhtml) .then(function (xhtml) { const foreignObjectSizing = (util.isDimensionMissing(width) ? ' width="100%"' : ` width="${width}"`) + (util.isDimensionMissing(height) ? ' height="100%"' : ` height="${height}"`); const svgSizing = (util.isDimensionMissing(width) ? '' : ` width="${width}"`) + (util.isDimensionMissing(height) ? '' : ` height="${height}"`); return `<svg xmlns="http://www.w3.org/2000/svg"${svgSizing}><foreignObject${foreignObjectSizing}>${xhtml}</foreignObject></svg>`; }) .then(function (svg) { return `data:image/svg+xml;charset=utf-8,${svg}`; }); } } /** * @param {Node} node - The DOM Node object to render * @param {Object} options - Rendering options, @see {@link toSvg} * @return {Promise} - A promise that is fulfilled with a Uint8Array containing RGBA pixel data. * */ function toPixelData(node, options) { return draw(node, options).then(function (canvas) { return canvas .getContext('2d') .getImageData(0, 0, util.width(node), util.height(node)).data; }); } /** * @param {Node} node - The DOM Node object to render * @param {Object} options - Rendering options, @see {@link toSvg} * @return {Promise} - A promise that is fulfilled with a PNG image data URL * */ function toPng(node, options) { return draw(node, options).then(function (canvas) { return canvas.toDataURL(); }); } /** * @param {Node} node - The DOM Node object to render * @param {Object} options - Rendering options, @see {@link toSvg} * @return {Promise} - A promise that is fulfilled with a JPEG image data URL * */ function toJpeg(node, options) { return draw(node, options).then(function (canvas) { return canvas.toDataURL( 'image/jpeg', (options ? options.quality : undefined) || 1.0 ); }); } /** * @param {Node} node - The DOM Node object to render * @param {Object} options - Rendering options, @see {@link toSvg} * @return {Promise} - A promise that is fulfilled with a PNG image blob * */ function toBlob(node, options) { return draw(node, options).then(util.canvasToBlob); } /** * @param {Node} node - The DOM Node object to render * @param {Object} options - Rendering options, @see {@link toSvg} * @return {Promise} - A promise that is fulfilled with a canvas object * */ function toCanvas(node, options) { return draw(node, options); } function copyOptions(options) { // Copy options to impl options for use in impl if (typeof options.copyDefaultStyles === 'undefined') { domtoimage.impl.options.copyDefaultStyles = defaultOptions.copyDefaultStyles; } else { domtoimage.impl.options.copyDefaultStyles = options.copyDefaultStyles; } if (typeof options.imagePlaceholder === 'undefined') { domtoimage.impl.options.imagePlaceholder = defaultOptions.imagePlaceholder; } else { domtoimage.impl.options.imagePlaceholder = options.imagePlaceholder; } if (typeof options.cacheBust === 'undefined') { domtoimage.impl.options.cacheBust = defaultOptions.cacheBust; } else { domtoimage.impl.options.cacheBust = options.cacheBust; } if (typeof options.corsImg === 'undefined') { domtoimage.impl.options.corsImg = defaultOptions.corsImg; } else { domtoimage.impl.options.corsImg = options.corsImg; } if (typeof options.useCredentials === 'undefined') { domtoimage.impl.options.useCredentials = defaultOptions.useCredentials; } else { domtoimage.impl.options.useCredentials = options.useCredentials; } if (typeof options.useCredentialsFilters === 'undefined') { domtoimage.impl.options.useCredentialsFilters = defaultOptions.useCredentialsFilters; } else { domtoimage.impl.options.useCredentialsFilters = options.useCredentialsFilters; } if (typeof options.httpTimeout === 'undefined') { domtoimage.impl.options.httpTimeout = defaultOptions.httpTimeout; } else { domtoimage.impl.options.httpTimeout = options.httpTimeout; } if (typeof options.styleCaching === 'undefined') { domtoimage.impl.options.styleCaching = defaultOptions.styleCaching; } else { domtoimage.impl.options.styleCaching = options.styleCaching; } } function draw(domNode, options) { options = options || {}; return toSvg(domNode, options) .then(util.makeImage) .then(function (image) { const scale = typeof options.scale !== 'number' ? 1 : options.scale; const canvas = newCanvas(domNode, scale); const ctx = canvas.getContext('2d'); ctx.msImageSmoothingEnabled = false; ctx.imageSmoothingEnabled = false; if (image) { ctx.scale(scale, scale); ctx.drawImage(image, 0, 0); } return canvas; }); function newCanvas(node, scale) { let width = options.width || util.width(node); let height = options.height || util.height(node); // per https://www.w3.org/TR/CSS2/visudet.html#inline-replaced-width the default width should be 300px if height // not set, otherwise should be 2:1 aspect ratio for whatever height is specified if (util.isDimensionMissing(width)) { width = util.isDimensionMissing(height) ? 300 : height * 2.0; } if (util.isDimensionMissing(height)) { height = width / 2.0; } const canvas = document.createElement('canvas'); canvas.width = width * scale; canvas.height = height * scale; if (options.bgcolor) { const ctx = canvas.getContext('2d'); ctx.fillStyle = options.bgcolor; ctx.fillRect(0, 0, canvas.width, canvas.height); } return canvas; } } let sandbox = null; function cloneNode(node, options, parentComputedStyles, ownerWindow) { const filter = options.filter; if ( node === sandbox || util.isHTMLScriptElement(node) || util.isHTMLStyleElement(node) || util.isHTMLLinkElement(node) || (parentComputedStyles !== null && filter && !filter(node)) ) { return Promise.resolve(); } return Promise.resolve(node) .then(makeNodeCopy) .then(adjustCloneBefore) .then(function (clone) { return cloneChildren(clone, getParentOfChildren(node)); }) .then(adjustCloneAfter) .then(function (clone) { return processClone(clone, node); }); function makeNodeCopy(original) { if (util.isHTMLCanvasElement(original)) { return util.makeImage(original.toDataURL()); } return original.cloneNode(false); } function adjustCloneBefore(clone) { if (options.adjustClonedNode) { options.adjustClonedNode(node, clone, false); } return Promise.resolve(clone); } function adjustCloneAfter(clone) { if (options.adjustClonedNode) { options.adjustClonedNode(node, clone, true); } return Promise.resolve(clone); } function getParentOfChildren(original) { if (util.isElementHostForOpenShadowRoot(original)) { return original.shadowRoot; // jump "down" to #shadow-root } return original; } function cloneChildren(clone, original) { const originalChildren = getRenderedChildren(original); let done = Promise.resolve(); if (originalChildren.length !== 0) { const originalComputedStyles = getComputedStyle( getRenderedParent(original) ); util.asArray(originalChildren).forEach(function (originalChild) { done = done.then(function () { return cloneNode( originalChild, options, originalComputedStyles, ownerWindow ).then(function (clonedChild) { if (clonedChild) { clone.appendChild(clonedChild); } }); }); }); } return done.then(function () { return clone; }); function getRenderedParent(original) { if (util.isShadowRoot(original)) { return original.host; // jump up from #shadow-root to its parent <element> } return original; } function getRenderedChildren(original) { if (util.isShadowSlotElement(original)) { const assignedNodes = original.assignedNodes(); if (assignedNodes && assignedNodes.length > 0) return assignedNodes; // shadow DOM <slot> has "assigned nodes" as rendered children } return original.childNodes; } } function processClone(clone, original) { if (!util.isElement(clone) || util.isShadowSlotElement(original)) { return Promise.resolve(clone); } return Promise.resolve() .then(cloneStyle) .then(clonePseudoElements) .then(copyUserInput) .then(fixSvg) .then(fixResponsiveImages) .then(function () { return clone; }); function fixResponsiveImages() { if (util.isHTMLImageElement(clone)) { // Remove lazy-loading and responsive attributes clone.removeAttribute('loading'); // If the original had srcset or sizes, set src to the resolved image if (original.srcset || original.sizes) { clone.removeAttribute('srcset'); clone.removeAttribute('sizes'); // Use currentSrc if available, otherwise fallback to src clone.src = original.currentSrc || original.src; } } } function cloneStyle() { copyStyle(original, clone); function copyFont(source, target) { target.font = source.font; target.fontFamily = source.fontFamily; target.fontFeatureSettings = source.fontFeatureSettings; target.fontKerning = source.fontKerning; target.fontSize = source.fontSize; target.fontStretch = source.fontStretch; target.fontStyle = source.fontStyle; target.fontVariant = source.fontVariant; target.fontVariantCaps = source.fontVariantCaps; target.fontVariantEastAsian = source.fontVariantEastAsian; target.fontVariantLigatures = source.fontVariantLigatures; target.fontVariantNumeric = source.fontVariantNumeric; target.fontVariationSettings = source.fontVariationSettings; target.fontWeight = source.fontWeight; } function copyStyle(sourceElement, targetElement) { const sourceComputedStyles = getComputedStyle(sourceElement); if (sourceComputedStyles.cssText) { targetElement.style.cssText = sourceComputedStyles.cssText; copyFont(sourceComputedStyles, targetElement.style); // here we re-assign the font props. } else { copyUserComputedStyleFast( options, sourceElement, sourceComputedStyles, parentComputedStyles, targetElement ); // Remove positioning of initial element, which stops them from being captured correctly if (parentComputedStyles === null) { [ 'inset-block', 'inset-block-start', 'inset-block-end', ].forEach((prop) => targetElement.style.removeProperty(prop)); ['left', 'right', 'top', 'bottom'].forEach((prop) => { if (targetElement.style.getPropertyValue(prop)) { targetElement.style.setProperty(prop, '0px'); } }); } } } } function clonePseudoElements() { const cloneClassName = util.uid(); [':before', ':after'].forEach(function (element) { clonePseudoElement(element); }); function clonePseudoElement(element) { const style = getComputedStyle(original, element); const content = style.getPropertyValue('content'); if (content === '' || content === 'none') { return; } const currentClass = clone.getAttribute('class') || ''; clone.setAttribute('class', `${currentClass} ${cloneClassName}`); const styleElement = document.createElement('style'); styleElement.appendChild(formatPseudoElementStyle()); clone.appendChild(styleElement); function formatPseudoElementStyle() { const selector = `.${cloneClassName}:${element}`; const cssText = style.cssText ? formatCssText() : formatCssProperties(); return document.createTextNode(`${selector}{${cssText}}`); function formatCssText() { return `${style.cssText} content: ${content};`; } function formatCssProperties() { const styleText = util .asArray(style) .map(formatProperty) .join('; '); return `${styleText};`; function formatProperty(name) { const propertyValue = style.getPropertyValue(name); const propertyPriority = style.getPropertyPriority(name) ? ' !important' : ''; return `${name}: ${propertyValue}${propertyPriority}`; } } } } } function copyUserInput() { if (util.isHTMLTextAreaElement(original)) { clone.innerHTML = original.value; } if (util.isHTMLInputElement(original)) { clone.setAttribute('value', original.value); } } function fixSvg() { if (util.isSVGElement(clone)) { clone.setAttribute('xmlns', 'http://www.w3.org/2000/svg'); if (util.isSVGRectElement(clone)) { ['width', 'height'].forEach(function (attribute) { const value = clone.getAttribute(attribute); if (value) { clone.style.setProperty(attribute, value); } }); } } } } } function embedFonts(node) { return fontFaces.resolveAll().then(function (cssText) { if (cssText !== '') { const styleNode = document.createElement('style'); node.appendChild(styleNode); styleNode.appendChild(document.createTextNode(cssText)); } return node; }); } function inlineImages(node) { return images.inlineAll(node).then(function () { return node; }); } function newUtil() { let uid_index = 0; return { escape: escapeRegEx, isDataUrl: isDataUrl, canvasToBlob: canvasToBlob, resolveUrl: resolveUrl, getAndEncode: getAndEncode, uid: uid, asArray: asArray, escapeXhtml: escapeXhtml, makeImage: makeImage, width: width, height: height, getWindow: getWindow, isElement: isElement, isElementHostForOpenShadowRoot: isElementHostForOpenShadowRoot, isShadowRoot: isShadowRoot, isInShadowRoot: isInShadowRoot, isHTMLElement: isHTMLElement, isHTMLCanvasElement: isHTMLCanvasElement, isHTMLInputElement: isHTMLInputElement, isHTMLImageElement: isHTMLImageElement, isHTMLLinkElement: isHTMLLinkElement, isHTMLScriptElement: isHTMLScriptElement, isHTMLStyleElement: isHTMLStyleElement, isHTMLTextAreaElement: isHTMLTextAreaElement, isShadowSlotElement: isShadowSlotElement, isSVGElement: isSVGElement, isSVGRectElement: isSVGRectElement, isDimensionMissing: isDimensionMissing, }; function getWindow(node) { const ownerDocument = node ? node.ownerDocument : undefined; return ( (ownerDocument ? ownerDocument.defaultView : undefined) || window || global ); } function isElementHostForOpenShadowRoot(value) { return isElement(value) && value.shadowRoot !== null; } function isShadowRoot(value) { return value instanceof getWindow(value).ShadowRoot; } function isInShadowRoot(value) { // not calling the method, getting the method if (value === null || value === undefined || value.getRootNode === undefined) return false; return isShadowRoot(value.getRootNode()); } function isElement(value) { return value instanceof getWindow(value).Element; } function isHTMLCanvasElement(value) { return value instanceof getWindow(value).HTMLCanvasElement; } function isHTMLElement(value) { return value instanceof getWindow(value).HTMLElement; } function isHTMLImageElement(value) { return value instanceof getWindow(value).HTMLImageElement; } function isHTMLInputElement(value) { return value instanceof getWindow(value).HTMLInputElement; } function isHTMLLinkElement(value) { return value instanceof getWindow(value).HTMLLinkElement; } function isHTMLScriptElement(value) { return value instanceof getWindow(value).HTMLScriptElement; } function isHTMLStyleElement(value) { return value instanceof getWindow(value).HTMLStyleElement; } function isHTMLTextAreaElement(value) { return value instanceof getWindow(value).HTMLTextAreaElement; } function isShadowSlotElement(value) { return ( isInShadowRoot(value) && value instanceof getWindow(value).HTMLSlotElement ); } function isSVGElement(value) { return value instanceof getWindow(value).SVGElement; } function isSVGRectElement(value) { return value instanceof getWindow(value).SVGRectElement; } function isDataUrl(url) { return url.search(/^(data:)/) !== -1; } function isDimensionMissing(value) { return isNaN(value) || value <= 0; } function asBlob(canvas) { return new Promise(function (resolve) { const binaryString = atob(canvas.toDataURL().split(',')[1]); const length = binaryString.length; const binaryArray = new Uint8Array(length); for (let i = 0; i < length; i++) { binaryArray[i] = binaryString.charCodeAt(i); } resolve( new Blob([binaryArray], { type: 'image/png', }) ); }); } function canvasToBlob(canvas) { if (canvas.toBlob) { return new Promise(function (resolve) { canvas.toBlob(resolve); }); } return asBlob(canvas); } function resolveUrl(url, baseUrl) { const doc = document.implementation.createHTMLDocument(); const base = doc.createElement('base'); doc.head.appendChild(base); const a = doc.createElement('a'); doc.body.appendChild(a); base.href = baseUrl; a.href = url; return a.href; } function uid() { return `u${fourRandomChars()}${uid_index++}`; function fourRandomChars() { /* see https://stackoverflow.com/a/6248722/2519373 */ return `0000${((Math.random() * Math.pow(36, 4)) << 0).toString( 36 )}`.slice(-4); } } function makeImage(uri) { if (uri === 'data:,') { return Promise.resolve(); } return new Promise(function (resolve, reject) { // Create an SVG element to house the image const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); // and create the Image element to insert into that wrapper const image = new Image(); if (domtoimage.impl.options.useCredentials) { image.crossOrigin = 'use-credentials'; } image.onload = function () { // Cleanup: remove theimage from the document document.body.removeChild(svg); if (window && window.requestAnimationFrame) { // In order to work around a Firefox bug (webcompat/web-bugs#119834) we // need to wait one extra frame before it's safe to read the image data. window.requestAnimationFrame(function () { resolve(image); }); } else { // If we don't have a window or requestAnimationFrame function proceed immediately. resolve(image); } }; image.onerror = (error) => { // Cleanup: remove the image from the document document.body.removeChild(svg); reject(error); }; svg.appendChild(image); image.src = uri; // Add the SVG to the document body (invisible) document.body.appendChild(svg); }); } function getAndEncode(url) { let cacheEntry = domtoimage.impl.urlCache.find(function (el) { return el.url === url; }); if (!cacheEntry) { cacheEntry = { url: url, promise: null, }; domtoimage.impl.urlCache.push(cacheEntry); } if (cacheEntry.promise === null) { if (domtoimage.impl.options.cacheBust) { // Cache bypass so we don't have CORS issues with cached images // Source: https://developer.mozilla.org/en/docs/Web/API/XMLHttpRequest/Using_XMLHttpRequest#Bypassing_the_cache url += (/\?/.test(url) ? '&' : '?') + new Date().getTime(); } cacheEntry.promise = new Promise(function (resolve) { const xhr = new XMLHttpRequest(); xhr.timeout = domtoimage.impl.options.httpTimeout; xhr.onerror = placehold; xhr.ontimeout = placehold; xhr.onloadend = function () { if (xhr.readyState === XMLHttpRequest.DONE) { const status = xhr.status; // In local files, status is 0 upon success in Mozilla Firefox if ( (status === 0 && url.toLowerCase().startsWith('file://')) || (status >= 200 && status <= 300 && xhr.response !== null) ) { const response = xhr.response; if (!(response instanceof Blob)) { fail( 'Expected response to be a Blob, but got: ' + typeof response ); } const reader = new FileReader(); reader.onloadend = function () { const result = reader.result; resolve(result); }; try { reader.readAsDataURL(response); } catch (ex) { fail( 'Failed to read the response as Data URL: ' + ex.toString() ); } } else { placehold(); } } }; function fail(message) { console.error(message); resolve(''); } function placehold() { const placeholder = domtoimage.impl.options.imagePlaceholder; if (placeholder) { resolve(placeholder); } else { fail('Status:' + xhr.status + ' while fetching resource: ' + url); } } function handleJson(data) { try { return JSON.parse(JSON.stringify(data)); } catch (e) { fail('corsImg.data is missing or invalid:' + e.toString()); } } if (domtoimage.impl.options.useCredentialsFilters.length > 0) { domtoimage.impl.options.useCredentials = domtoimage.impl.options.useCredentialsFilters.filter( (credentialsFilter) => url.search(credentialsFilter) >= 0 ).length > 0; } if (domtoimage.impl.options.useCredentials) { xhr.withCredentials = true; } if ( domtoimage.impl.options.corsImg && url.indexOf('http') === 0 && url.indexOf(window.location.origin) === -1 ) { const method = ( domtoimage.impl.options.corsImg.method || 'GET' ).toUpperCase() === 'POST' ? 'POST' : 'GET'; xhr.open( method, (domtoimage.impl.options.corsImg.url || '').replace( '#{cors}', url ), true ); let isJson = false; const headers = domtoimage.impl.options.corsImg.headers || {}; Object.keys(headers).forEach(function (key) { if (headers[key].indexOf('application/json') !== -1) { isJson = true; } xhr.setRequestHeader(key, headers[key]); }); const corsData = handleJson( domtoimage.impl.options.corsImg.data || '' ); Object.keys(corsData).forEach(function (key) { if (typeof corsData[key] === 'string') { corsData[key] = corsData[key].replace('#{cors}', url); } }); xhr.responseType = 'blob'; xhr.send(isJson ? JSON.stringify(corsData) : corsData); } else { xhr.open('GET', url, true); xhr.responseType = 'blob'; xhr.send(); } }); } return cacheEntry.promise; } function escapeRegEx(string) { return string.replace(/([.*+?^${}()|[\]/\\])/g, '\\$1'); } function asArray(arrayLike) { const array = []; const length = arrayLike.length; for (let i = 0; i < length; i++) { array.push(arrayLike[i]); } return array; } function escapeXhtml(string) { return string.replace(/%/g, '%25').replace(/#/g, '%23').replace(/\n/g, '%0A'); } function width(node) { const width = px(node, 'width'); if (!isNaN(width)) return width; const leftBorder = px(node, 'border-left-width'); const rightBorder = px(node, 'border-right-width'); return node.scrollWidth + leftBorder + rightBorder; } function height(node) { const height = px(node, 'height'); if (!isNaN(height)) return height; const topBorder = px(node, 'border-top-width'); const bottomBorder = px(node, 'border-bottom-width'); return node.scrollHeight + topBorder + bottomBorder; } function px(node, styleProperty) { if (node.nodeType === ELEMENT_NODE) { let value = getComputedStyle(node).getPropertyValue(styleProperty); if (value.slice(-2) === 'px') { value = value.slice(0, -2); return parseFloat(value); } } return NaN; } } function newInliner() { const URL_REGEX = /url\(\s*(["']?)((?:\\.|[^\\)])+)\1\s*\)/gm; return { inlineAll: inlineAll, shouldProcess: shouldProcess, impl: { readUrls: readUrls, inline: inline, urlAsRegex: urlAsRegex, }, }; function shouldProcess(string) { return string.search(URL_REGEX) !== -1; } function readUrls(string) { const result = []; let match; while ((match = URL_REGEX.exec(string)) !== null) { result.push(match[2]); } return result.filter(function (url) { return !util.isDataUrl(url); }); } function urlAsRegex(urlValue) { return new RegExp(`url\\((["']?)(${util.escape(urlValue)})\\1\\)`, 'gm'); } function inline(string, url, baseUrl, get) { return Promise.resolve(url) .then(function (urlValue) { return baseUrl ? util.resolveUrl(urlValue, baseUrl) : urlValue; }) .then(get || util.getAndEncode) .then(function (dataUrl) { const pattern = urlAsRegex(url); return string.replace(pattern, `url($1${dataUrl}$1)`); }); } function inlineAll(string, baseUrl, get) { if (nothingToInline()) { return Promise.resolve(string); } return Promise.resolve(string) .then(readUrls) .then(function (urls) { let done = Promise.resolve(string); urls.forEach(function (url) { done = done.then(function (prefix) { return inline(prefix, url, baseUrl, get); }); }); return done; }); function nothingToInline() { return !shouldProcess(string); } } } function newFontFaces() { return { resolveAll: resolveAll, impl: { readAll: readAll, }, }; function resolveAll() { return readAll() .then(function (webFonts) { return Promise.all( webFonts.map(function (webFont) { return webFont.resolve(); }) ); }) .then(function (cssStrings) { return cssStrings.join('\n'); }); } function readAll() { return Promise.resolve(util.asArray(document.styleSheets)) .then(getCssRules) .then(selectWebFontRules) .then(function (rules) { return rules.map(newWebFont); }); function selectWebFontRules(cssRules) { return cssRules .filter(function (rule) { return rule.type === CSSRule.FONT_FACE_RULE; }) .filter(function (rule) { return inliner.shouldProcess(rule.style.getPropertyValue('src')); }); } function getCssRules(styleSheets) { const cssRules = []; styleSheets.forEach(function (sheet) { const sheetProto = Object.getPrototypeOf(sheet); // NOSONAR if (Object.prototype.hasOwnProperty.call(sheetProto, 'cssRules')) { try { util.asArray(sheet.cssRules || []).forEach( cssRules.push.bind(cssRules) ); } catch (e) { console.error( 'domtoimage: Error while reading CSS rules from: ' + sheet.href, e.toString() ); } } }); return cssRules; } function newWebFont(webFontRule) { return { resolve: function resolve() { // NOSONAR const baseUrl = (webFontRule.parentStyleSheet || {}).href; return inliner.inlineAll(webFontRule.cssText, baseUrl); }, src: function () { return webFontRule.style.getPropertyValue('src'); }, }; } } } function newImages() { return { inlineAll: inlineAll, impl: { newImage: newImage, }, }; function newImage(element) { return { inline: inline, }; function inline(get) { if (util.isDataUrl(element.src)) { return Promise.resolve(); } return Promise.resolve(element.src) .then(get || util.getAndEncode) .then(function (dataUrl) { return new Promise(function (resolve) { element.onload = resolve; // for any image with invalid src(such as <img src />), just ignore it element.onerror = resolve; element.src = dataUrl; }); }); } } function inlineAll(node) { if (!util.isElement(node)) { return Promise.resolve(node); } return inlineCSSProperty(node).then(function () { if (util.isHTMLImageElement(node)) { return newImage(node).inline(); } else { return Promise.all( util.asArray(node.childNodes).map(function (child) { return inlineAll(child); }) ); } }); function inlineCSSProperty(node) { const properties = ['background', 'background-image']; const inliningTasks = properties.map(function (propertyName) { const value = node.style.getPropertyValue(propertyName); const priority = node.style.getPropertyPriority(propertyName); if (!value) { return Promise.resolve(); } return inliner.inlineAll(value).then(function (inlinedValue) { node.style.setProperty(propertyName, inlinedValue, priority); }); }); return Promise.all(inliningTasks).then(function () { return node; }); } } } function setStyleProperty(targetStyle, name, value, priority) { const needs_prefixing = ['background-clip'].indexOf(name) >= 0; if (priority) { targetStyle.setProperty(name, value, priority); if (needs_prefixing) { targetStyle.setProperty(`-webkit-${name}`, value, priority); } } else { targetStyle.setProperty(name, value); if (needs_prefixing) { targetStyle.setProperty(`-webkit-${name}`, value); } } } function copyUserComputedStyleFast( options, sourceElement, sourceComputedStyles, parentComputedStyles, targetElement ) { const defaultStyle = domtoimage.impl.options.copyDefaultStyles ? getDefaultStyle(options, sourceElement) : {}; const targetStyle = targetElement.style; util.asArray(sourceComputedStyles).forEach(function (name) { if (options.filterStyles) { if (!options.filterStyles(sourceElement, name)) { return; } } const sourceValue = sourceComputedStyles.getPropertyValue(name); const defaultValue = defaultStyle[name]; const parentValue = parentComputedStyles ? parentComputedStyles.getPropertyValue(name)