diff --git a/CHANGELOG.md b/CHANGELOG.md index 3201b22788..be768cfac5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,10 +20,11 @@ All notable changes to the Wazuh app project will be documented in this file. - Added support for agents to Office 365 [#6558](https://github.com/wazuh/wazuh-dashboard-plugins/pull/6558) - Added pinned agent data validation when rendering the Inventory data, Stats and Configuration tabs in Agent preview of Endpoints Summary [#6800](https://github.com/wazuh/wazuh-dashboard-plugins/pull/6800) - Added wz-link component to make redirections [#6848](https://github.com/wazuh/wazuh-dashboard-plugins/pull/6848) +- Add embedded and customized `dom-to-image-more` dependency [#6902](https://github.com/wazuh/wazuh-dashboard-plugins/pull/6902) ### Changed -- Removed embedded discover [#6120](https://github.com/wazuh/wazuh-dashboard-plugins/pull/6120) [#6235](https://github.com/wazuh/wazuh-dashboard-plugins/pull/6235) [#6254](https://github.com/wazuh/wazuh-dashboard-plugins/pull/6254) [#6285](https://github.com/wazuh/wazuh-dashboard-plugins/pull/6285) [#6288](https://github.com/wazuh/wazuh-dashboard-plugins/pull/6288) [#6290](https://github.com/wazuh/wazuh-dashboard-plugins/pull/6290) [#6289](https://github.com/wazuh/wazuh-dashboard-plugins/pull/6289) [#6286](https://github.com/wazuh/wazuh-dashboard-plugins/pull/6286) [#6275](https://github.com/wazuh/wazuh-dashboard-plugins/pull/6275) [#6287](https://github.com/wazuh/wazuh-dashboard-plugins/pull/6287) [#6297](https://github.com/wazuh/wazuh-dashboard-plugins/pull/6297) [#6291](https://github.com/wazuh/wazuh-dashboard-plugins/pull/6287) [#6459](https://github.com/wazuh/wazuh-dashboard-plugins/pull/6459) [#6434](https://github.com/wazuh/wazuh-dashboard-plugins/pull/6434) [#6504](https://github.com/wazuh/wazuh-dashboard-plugins/pull/6504) [#6649](https://github.com/wazuh/wazuh-dashboard-plugins/pull/6649) [#6506](https://github.com/wazuh/wazuh-dashboard-plugins/pull/6506) [#6537](https://github.com/wazuh/wazuh-dashboard-plugins/pull/6537) [#6528](https://github.com/wazuh/wazuh-dashboard-plugins/pull/6528) [#6675](https://github.com/wazuh/wazuh-dashboard-plugins/pull/6675) [#6674](https://github.com/wazuh/wazuh-dashboard-plugins/pull/6674) [#6558](https://github.com/wazuh/wazuh-dashboard-plugins/pull/6558) [#6685](https://github.com/wazuh/wazuh-dashboard-plugins/pull/6685) [#6691](https://github.com/wazuh/wazuh-dashboard-plugins/pull/6691) [#6712](https://github.com/wazuh/wazuh-dashboard-plugins/pull/6712) [#6734](https://github.com/wazuh/wazuh-dashboard-plugins/pull/6734) [#6746](https://github.com/wazuh/wazuh-dashboard-plugins/pull/6746) [#6752](https://github.com/wazuh/wazuh-dashboard-plugins/pull/6752) [#6753](https://github.com/wazuh/wazuh-dashboard-plugins/pull/6753) [#6756](https://github.com/wazuh/wazuh-dashboard-plugins/pull/6756) [#6771](https://github.com/wazuh/wazuh-dashboard-plugins/pull/6771) [#6792](https://github.com/wazuh/wazuh-dashboard-plugins/pull/6792) [#6845](https://github.com/wazuh/wazuh-dashboard-plugins/pull/6845) [#6857](https://github.com/wazuh/wazuh-dashboard-plugins/pull/6857) [#6847](https://github.com/wazuh/wazuh-dashboard-plugins/pull/6847) [#6865](https://github.com/wazuh/wazuh-dashboard-plugins/pull/6865) [#6848](https://github.com/wazuh/wazuh-dashboard-plugins/pull/6848) [#6843](https://github.com/wazuh/wazuh-dashboard-plugins/pull/6843) [#6878](https://github.com/wazuh/wazuh-dashboard-plugins/pull/6878) [#6883](https://github.com/wazuh/wazuh-dashboard-plugins/pull/6883) [#6889](https://github.com/wazuh/wazuh-dashboard-plugins/pull/6889) +- Removed embedded discover [#6120](https://github.com/wazuh/wazuh-dashboard-plugins/pull/6120) [#6235](https://github.com/wazuh/wazuh-dashboard-plugins/pull/6235) [#6254](https://github.com/wazuh/wazuh-dashboard-plugins/pull/6254) [#6285](https://github.com/wazuh/wazuh-dashboard-plugins/pull/6285) [#6288](https://github.com/wazuh/wazuh-dashboard-plugins/pull/6288) [#6290](https://github.com/wazuh/wazuh-dashboard-plugins/pull/6290) [#6289](https://github.com/wazuh/wazuh-dashboard-plugins/pull/6289) [#6286](https://github.com/wazuh/wazuh-dashboard-plugins/pull/6286) [#6275](https://github.com/wazuh/wazuh-dashboard-plugins/pull/6275) [#6287](https://github.com/wazuh/wazuh-dashboard-plugins/pull/6287) [#6297](https://github.com/wazuh/wazuh-dashboard-plugins/pull/6297) [#6291](https://github.com/wazuh/wazuh-dashboard-plugins/pull/6287) [#6459](https://github.com/wazuh/wazuh-dashboard-plugins/pull/6459) [#6434](https://github.com/wazuh/wazuh-dashboard-plugins/pull/6434) [#6504](https://github.com/wazuh/wazuh-dashboard-plugins/pull/6504) [#6649](https://github.com/wazuh/wazuh-dashboard-plugins/pull/6649) [#6506](https://github.com/wazuh/wazuh-dashboard-plugins/pull/6506) [#6537](https://github.com/wazuh/wazuh-dashboard-plugins/pull/6537) [#6528](https://github.com/wazuh/wazuh-dashboard-plugins/pull/6528) [#6675](https://github.com/wazuh/wazuh-dashboard-plugins/pull/6675) [#6674](https://github.com/wazuh/wazuh-dashboard-plugins/pull/6674) [#6558](https://github.com/wazuh/wazuh-dashboard-plugins/pull/6558) [#6685](https://github.com/wazuh/wazuh-dashboard-plugins/pull/6685) [#6691](https://github.com/wazuh/wazuh-dashboard-plugins/pull/6691) [#6712](https://github.com/wazuh/wazuh-dashboard-plugins/pull/6712) [#6734](https://github.com/wazuh/wazuh-dashboard-plugins/pull/6734) [#6746](https://github.com/wazuh/wazuh-dashboard-plugins/pull/6746) [#6752](https://github.com/wazuh/wazuh-dashboard-plugins/pull/6752) [#6753](https://github.com/wazuh/wazuh-dashboard-plugins/pull/6753) [#6756](https://github.com/wazuh/wazuh-dashboard-plugins/pull/6756) [#6771](https://github.com/wazuh/wazuh-dashboard-plugins/pull/6771) [#6792](https://github.com/wazuh/wazuh-dashboard-plugins/pull/6792) [#6845](https://github.com/wazuh/wazuh-dashboard-plugins/pull/6845) [#6857](https://github.com/wazuh/wazuh-dashboard-plugins/pull/6857) [#6847](https://github.com/wazuh/wazuh-dashboard-plugins/pull/6847) [#6865](https://github.com/wazuh/wazuh-dashboard-plugins/pull/6865) [#6848](https://github.com/wazuh/wazuh-dashboard-plugins/pull/6848) [#6843](https://github.com/wazuh/wazuh-dashboard-plugins/pull/6843) [#6878](https://github.com/wazuh/wazuh-dashboard-plugins/pull/6878) [#6883](https://github.com/wazuh/wazuh-dashboard-plugins/pull/6883) [#6889](https://github.com/wazuh/wazuh-dashboard-plugins/pull/6889) [#6902](https://github.com/wazuh/wazuh-dashboard-plugins/pull/6902) - Allow editing groups for an agent from Endpoints Summary [#6250](https://github.com/wazuh/wazuh-dashboard-plugins/pull/6250) - Change how the configuration is managed in the backend side [#6337](https://github.com/wazuh/wazuh-dashboard-plugins/pull/6337) [#6519](https://github.com/wazuh/wazuh-dashboard-plugins/pull/6519) [#6573](https://github.com/wazuh/wazuh-dashboard-plugins/pull/6573) - Change the view of API is down and check connection to Server APIs application [#6337](https://github.com/wazuh/wazuh-dashboard-plugins/pull/6337) @@ -81,6 +82,7 @@ All notable changes to the Wazuh app project will be documented in this file. - Removed the `App logs` application [#6161](https://github.com/wazuh/wazuh-dashboard-plugins/pull/6161) - Removed API endpoint GET /utils/logs/ui [#6161](https://github.com/wazuh/wazuh-dashboard-plugins/pull/6161) - Removed API endpoint GET /utils/logs [#6161](https://github.com/wazuh/wazuh-dashboard-plugins/pull/6161) +- Removed embedded `dom-to-image` dependency [#6902](https://github.com/wazuh/wazuh-dashboard-plugins/pull/6902) ## Wazuh v4.8.1 - OpenSearch Dashboards 2.10.0 - Revision 04 diff --git a/plugins/main/public/app.js b/plugins/main/public/app.js index efc0d877fd..1fe4f431c0 100644 --- a/plugins/main/public/app.js +++ b/plugins/main/public/app.js @@ -15,5 +15,3 @@ import './styles'; import './utils/fontawesome/scss/font-awesome.scss'; // Dev tools import './utils/codemirror'; -// Require lib to dashboards PDFs -require('./utils/dom-to-image.js'); diff --git a/plugins/main/public/components/common/modules/buttons/generate_report.tsx b/plugins/main/public/components/common/modules/buttons/generate_report.tsx index 311658588f..097bae7d6a 100644 --- a/plugins/main/public/components/common/modules/buttons/generate_report.tsx +++ b/plugins/main/public/components/common/modules/buttons/generate_report.tsx @@ -38,7 +38,9 @@ export const ButtonModuleGenerateReport = connect(mapStateToProps)( const defaultTextColor = '#DFE5EF'; //Patch to fix dark backgrounds in visualizations dark-mode pdf reports - const $labels = $('.euiButtonEmpty__text, .echLegendItem'); + const $labels = $( + '.euiButtonEmpty__text, .echLegendItem, div.mtrVis__value ~ div', + ); const $vizBackground = $('.echChartBackground'); const defaultVizBackground = $vizBackground.css('background-color'); diff --git a/plugins/main/public/components/overview/hipaa/dashboards/dashboard-panels.ts b/plugins/main/public/components/overview/hipaa/dashboards/dashboard-panels.ts index dcd53ff99c..ca68cf0d8b 100644 --- a/plugins/main/public/components/overview/hipaa/dashboards/dashboard-panels.ts +++ b/plugins/main/public/components/overview/hipaa/dashboards/dashboard-panels.ts @@ -366,10 +366,21 @@ const getVisStateStats = (indexPatternId: string) => { metric: { percentageMode: false, useRanges: false, - colorSchema: 'Green to Red', - metricColorMode: 'None', - colorsRange: [{ type: 'range', from: 0, to: 10000 }], - labels: { show: true }, + colorSchema: 'Greens', + metricColorMode: 'Labels', + colorsRange: [ + { + from: 0, + to: 0, + }, + { + from: 0, + to: 0, + }, + ], + labels: { + show: true, + }, invertColors: false, style: { bgFill: '#000', diff --git a/plugins/main/public/components/overview/nist/dashboards/dashboard-panels.ts b/plugins/main/public/components/overview/nist/dashboards/dashboard-panels.ts index 34d5069697..f7271f7f73 100644 --- a/plugins/main/public/components/overview/nist/dashboards/dashboard-panels.ts +++ b/plugins/main/public/components/overview/nist/dashboards/dashboard-panels.ts @@ -554,10 +554,21 @@ const getVisStateMetrics = (indexPatternId: string) => { metric: { percentageMode: false, useRanges: false, - colorSchema: 'Green to Red', - metricColorMode: 'None', - colorsRange: [{ type: 'range', from: 0, to: 10000 }], - labels: { show: true }, + colorSchema: 'Greens', + metricColorMode: 'Labels', + colorsRange: [ + { + from: 0, + to: 0, + }, + { + from: 0, + to: 0, + }, + ], + labels: { + show: true, + }, invertColors: false, style: { bgFill: '#000', @@ -722,10 +733,21 @@ const getVisStateAgentStats = (indexPatternId: string) => { metric: { percentageMode: false, useRanges: false, - colorSchema: 'Green to Red', - metricColorMode: 'None', - colorsRange: [{ type: 'range', from: 0, to: 10000 }], - labels: { show: true }, + colorSchema: 'Greens', + metricColorMode: 'Labels', + colorsRange: [ + { + from: 0, + to: 0, + }, + { + from: 0, + to: 0, + }, + ], + labels: { + show: true, + }, invertColors: false, style: { bgFill: '#000', diff --git a/plugins/main/public/components/overview/tsc/dashboards/dashboard-panels.ts b/plugins/main/public/components/overview/tsc/dashboards/dashboard-panels.ts index b7470cdc8b..488c7c0e68 100644 --- a/plugins/main/public/components/overview/tsc/dashboards/dashboard-panels.ts +++ b/plugins/main/public/components/overview/tsc/dashboards/dashboard-panels.ts @@ -547,82 +547,6 @@ const getVisStateRequirementsByAgent = (indexPatternId: string) => { ], }, }; - return { - title: 'Stats', - type: 'metric', - params: { - metric: { - percentageMode: false, - useRanges: false, - colorSchema: 'Green to Red', - metricColorMode: 'None', - colorsRange: [{ type: 'range', from: 0, to: 10000 }], - labels: { show: true }, - invertColors: false, - style: { - bgFill: '#000', - bgColor: false, - labelColor: false, - subText: '', - fontSize: 20, - }, - }, - dimensions: { - metrics: [ - { - type: 'vis_dimension', - accessor: 0, - format: { id: 'number', params: {} }, - }, - { - type: 'vis_dimension', - accessor: 1, - format: { id: 'number', params: {} }, - }, - ], - }, - addTooltip: true, - addLegend: false, - type: 'metric', - }, - uiState: {}, - data: { - searchSource: { - query: { - language: 'kuery', - query: '', - }, - filter: [], - index: indexPatternId, - }, - references: [ - { - name: 'kibanaSavedObjectMeta.searchSourceJSON.index', - type: 'index-pattern', - id: indexPatternId, - }, - ], - aggs: [ - { - id: '1', - enabled: true, - type: 'count', - schema: 'metric', - params: { customLabel: 'Total alerts' }, - }, - { - id: '2', - enabled: true, - type: 'max', - schema: 'metric', - params: { - field: 'rule.level', - customLabel: 'Max rule level detected', - }, - }, - ], - }, - }; }; const getVisStateAgentTopRuleGroups = (indexPatternId: string) => { diff --git a/plugins/main/public/react-services/reporting.js b/plugins/main/public/react-services/reporting.js index 2477701c6e..a5436236a5 100644 --- a/plugins/main/public/react-services/reporting.js +++ b/plugins/main/public/react-services/reporting.js @@ -19,7 +19,7 @@ import { UI_LOGGER_LEVELS } from '../../common/constants'; import { UI_ERROR_SEVERITIES } from './error-orchestrator/types'; import { getErrorOrchestrator } from './common-services'; import store from '../redux/store'; -import domtoimage from '../utils/dom-to-image'; +import domtoimage from '../utils/dom-to-image-more'; import dateMath from '@elastic/datemath'; import React from 'react'; import { EuiFlexGroup, EuiFlexItem, EuiButton, EuiLink } from '@elastic/eui'; @@ -116,7 +116,18 @@ export class ReportingService { return await Promise.all( Array.from(domVisualizations).map(async node => { return { - element: await domtoimage.toPng(node), + /* WORKAROUND: Defining the width and height resolves a bug + related to cropped screenshot on Firefox. + + This solution is based on + https://github.com/1904labs/dom-to-image-more/issues/160#issuecomment-1922491067 + + See https://github.com/wazuh/wazuh-dashboard-plugins/issues/6900#issuecomment-2275495245 + */ + element: await domtoimage.toPng(node, { + width: node.clientWidth, + height: node.clientHeight, + }), width: node.clientWidth, height: node.clientHeight, title: node?.parentNode?.parentNode?.parentNode?.querySelector( @@ -140,13 +151,13 @@ export class ReportingService { const timeFilter = dataSourceContext.time && dataSourceContext.indexPattern.timeFieldName ? buildRangeFilter( - { - name: dataSourceContext.indexPattern.timeFieldName, - type: 'date', - }, - dataSourceContext.time, - dataSourceContext.indexPattern, - ) + { + name: dataSourceContext.indexPattern.timeFieldName, + type: 'date', + }, + dataSourceContext.time, + dataSourceContext.indexPattern, + ) : null; // Build the filters to use in the server side // Based on https://github.com/opensearch-project/OpenSearch-Dashboards/blob/2.13.0/src/plugins/data/public/query/query_service.ts#L103-L113 @@ -166,12 +177,12 @@ export class ReportingService { tab === 'syscollector' ? { to: dataSourceContext.time.to, from: dataSourceContext.time.from } : { - to: dateMath.parse(dataSourceContext.time.to, { - roundUp: true, - forceNow: getForceNow(), - }), - from: dateMath.parse(dataSourceContext.time.from), - }; + to: dateMath.parse(dataSourceContext.time.to, { + roundUp: true, + forceNow: getForceNow(), + }), + from: dateMath.parse(dataSourceContext.time.from), + }; const data = { array: visualizations, diff --git a/plugins/main/public/utils/dom-to-image-more.js b/plugins/main/public/utils/dom-to-image-more.js new file mode 100644 index 0000000000..0aaa8de378 --- /dev/null +++ b/plugins/main/public/utils/dom-to-image-more.js @@ -0,0 +1,1550 @@ +/* +The MIT License (MIT) + +Copyright 2018 Marc Brooks +https://about.me/idisposable + +Copyright 2015 Anatolii Saienko +https://github.com/tsayen + +Copyright 2012 Paul Bakaus +http://paulbakaus.com/ + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +*/ + +// dom-to-image-more 3.3.1 +// It contains some customization by Wazuh +(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, + }; + + 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: {}, + }, + }; + + 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 {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) + * @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 || {}; + copyOptions(options); + let restorations = []; + return ( + Promise.resolve(node) + .then(ensureElement) + .then(function (clonee) { + return cloneNode(clonee, options, null, ownerWindow); + }) + // Wazuh + // Avoid downloading the embed fonts + // .then(embedFonts) + .then(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(node) { + let width = options.width || util.width(node); + let height = options.height || util.height(node); + + return Promise.resolve(node) + .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 `${xhtml}`; + }) + .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 + } + return original; + } + + function getRenderedChildren(original) { + if (util.isShadowSlotElement(original)) { + return original.assignedNodes(); // shadow DOM 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(function () { + return clone; + }); + + 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, + delay: delay, + 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) || + global || + window + ); + } + + function isElementHostForOpenShadowRoot(value) { + return isElement(value) && value.shadowRoot !== null; + } + + function isShadowRoot(value) { + return value instanceof getWindow(value).ShadowRoot; + } + + function isInShadowRoot(value) { + return ( + value !== null && + Object.prototype.hasOwnProperty.call(value, 'getRootNode') && + 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) { + const image = new Image(); + if (domtoimage.impl.options.useCredentials) { + image.crossOrigin = 'use-credentials'; + } + image.onload = function () { + 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 = reject; + image.src = uri; + }); + } + + 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 dont 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 httpTimeout = domtoimage.impl.options.httpTimeout; + const request = new XMLHttpRequest(); + + request.onreadystatechange = done; + request.ontimeout = timeout; + request.responseType = 'blob'; + request.timeout = httpTimeout; + + 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) { + request.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'; + + request.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; + } + request.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); + } + }); + + request.send(isJson ? JSON.stringify(corsData) : corsData); + } else { + request.open('GET', url, true); + request.send(); + } + + let placeholder; + if (domtoimage.impl.options.imagePlaceholder) { + const split = domtoimage.impl.options.imagePlaceholder.split(/,/); + if (split && split[1]) { + placeholder = split[1]; + } + } + + function done() { + if (request.readyState !== 4) { + return; + } + + if (request.status >= 300) { + if (placeholder) { + resolve(placeholder); + } else { + fail( + `cannot fetch resource: ${url}, status: ${request.status}`, + ); + } + + return; + } + + const encoder = new FileReader(); + encoder.onloadend = function () { + resolve(encoder.result); + }; + encoder.readAsDataURL(request.response); + } + + function timeout() { + if (placeholder) { + resolve(placeholder); + } else { + fail( + `timeout of ${httpTimeout}ms occured while fetching resource: ${url}`, + ); + } + } + + function handleJson(data) { + try { + return JSON.parse(JSON.stringify(data)); + } catch (e) { + fail('corsImg.data is missing or invalid'); + return; + } + } + + function fail(message) { + console.error(message); + resolve(''); + } + }); + } + return cacheEntry.promise; + } + + function escapeRegEx(string) { + return string.replace(/([.*+?^${}()|[]\/\\])/g, '\\$1'); + } + + function delay(ms) { + return function (arg) { + return new Promise(function (resolve) { + setTimeout(function () { + resolve(arg); + }, ms); + }); + }; + } + + 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\(['"]?([^'"]+?)['"]?\)/g; + + return { + inlineAll: inlineAll, + shouldProcess: shouldProcess, + impl: { + readUrls: readUrls, + inline: inline, + }, + }; + + 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[1]); + } + return result.filter(function (url) { + return !util.isDataUrl(url); + }); + } + + 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) { + return string.replace(urlAsRegex(url), `$1${dataUrl}$3`); + }); + + function urlAsRegex(urlValue) { + return new RegExp( + `(url\\(['"]?)(${util.escape(urlValue)})(['"]?\\))`, + 'g', + ); + } + } + + 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) { + if ( + Object.prototype.hasOwnProperty.call( + Object.getPrototypeOf(sheet), + '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() { + 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 ), 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) { + const sourceValue = sourceComputedStyles.getPropertyValue(name); + const defaultValue = defaultStyle[name]; + const parentValue = parentComputedStyles + ? parentComputedStyles.getPropertyValue(name) + : undefined; + + // Ignore setting style property on clone node, if already it has a style (through adjustCloneNode) + const targetValue = targetStyle.getPropertyValue(name); + if (targetValue) return; + + // If the style does not match the default, or it does not match the parent's, set it. We don't know which + // styles are inherited from the parent and which aren't, so we have to always check both. + if ( + sourceValue !== defaultValue || + (parentComputedStyles && sourceValue !== parentValue) + ) { + const priority = sourceComputedStyles.getPropertyPriority(name); + setStyleProperty(targetStyle, name, sourceValue, priority); + } + }); + } + + let removeDefaultStylesTimeoutId = null; + let tagNameDefaultStyles = {}; + + const ascentStoppers = [ + // these come from https://developer.mozilla.org/en-US/docs/Web/HTML/Block-level_elements + 'ADDRESS', + 'ARTICLE', + 'ASIDE', + 'BLOCKQUOTE', + 'DETAILS', + 'DIALOG', + 'DD', + 'DIV', + 'DL', + 'DT', + 'FIELDSET', + 'FIGCAPTION', + 'FIGURE', + 'FOOTER', + 'FORM', + 'H1', + 'H2', + 'H3', + 'H4', + 'H5', + 'H6', + 'HEADER', + 'HGROUP', + 'HR', + 'LI', + 'MAIN', + 'NAV', + 'OL', + 'P', + 'PRE', + 'SECTION', + 'SVG', + 'TABLE', + 'UL', + // this is some non-standard ones + 'math', // intentionally lowercase, thanks Safari + 'svg', // in case we have an svg embedded element + // these are ultimate stoppers in case something drastic changes in how the DOM works + 'BODY', + 'HEAD', + 'HTML', + ]; + + function getDefaultStyle(options, sourceElement) { + const tagHierarchy = computeTagHierarchy(sourceElement); + const tagKey = computeTagKey(tagHierarchy); + if (tagNameDefaultStyles[tagKey]) { + return tagNameDefaultStyles[tagKey]; + } + + // We haven't cached the answer for that hierachy yet, build a + // sandbox (if not yet created), fill it with the hierarchy that + // matters, and grab the default styles associated + const sandboxWindow = ensureSandboxWindow(); + const defaultElement = constructElementHierachy( + sandboxWindow.document, + tagHierarchy, + ); + const defaultStyle = computeStyleForDefaults(sandboxWindow, defaultElement); + destroyElementHierarchy(defaultElement); + + tagNameDefaultStyles[tagKey] = defaultStyle; + return defaultStyle; + + function computeTagHierarchy(sourceNode) { + const tagNames = []; + + do { + if (sourceNode.nodeType === ELEMENT_NODE) { + const tagName = sourceNode.tagName; + tagNames.push(tagName); + + if (ascentStoppers.includes(tagName)) { + break; + } + } + + sourceNode = sourceNode.parentNode; + } while (sourceNode); + + return tagNames; + } + + function computeTagKey(tagHierarchy) { + if (options.styleCaching === 'relaxed') { + // pick up only the ascent-stopping element tag and the element tag itself + /* jshint unused:true */ + return tagHierarchy + .filter((_, i, a) => i === 0 || i === a.length - 1) + .join('>'); + } + // for all other cases, fall back the the entire path + return tagHierarchy.join('>'); // it's like CSS + } + + function constructElementHierachy(sandboxDocument, tagHierarchy) { + let element = sandboxDocument.body; + do { + const childTagName = tagHierarchy.pop(); + const childElement = sandboxDocument.createElement(childTagName); + element.appendChild(childElement); + element = childElement; + } while (tagHierarchy.length > 0); + + // Ensure that there is some content, so that properties like margin are applied. + // we use zero-width space to handle FireFox adding a pixel + element.textContent = '\u200b'; + return element; + } + + function computeStyleForDefaults(sandboxWindow, defaultElement) { + const defaultStyle = {}; + const defaultComputedStyle = + sandboxWindow.getComputedStyle(defaultElement); + + // Copy styles to an object, making sure that 'width' and 'height' are given the default value of 'auto', since + // their initial value is always 'auto' despite that the default computed value is sometimes an absolute length. + util.asArray(defaultComputedStyle).forEach(function (name) { + defaultStyle[name] = + name === 'width' || name === 'height' + ? 'auto' + : defaultComputedStyle.getPropertyValue(name); + }); + return defaultStyle; + } + + function destroyElementHierarchy(element) { + do { + const parentElement = element.parentElement; + if (parentElement !== null) { + parentElement.removeChild(element); + } + element = parentElement; + } while (element && element.tagName !== 'BODY'); + } + } + + function ensureSandboxWindow() { + if (sandbox) { + return sandbox.contentWindow; + } + + // figure out how this document is defined (doctype and charset) + const charsetToUse = document.characterSet || 'UTF-8'; + const docType = document.doctype; + const docTypeDeclaration = docType + ? `' + : ''; + + // Create a hidden sandbox