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
JavaScript
(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)