OfflineExporting.js 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595
  1. /* *
  2. *
  3. * Client side exporting module
  4. *
  5. * (c) 2015 Torstein Honsi / Oystein Moseng
  6. *
  7. * License: www.highcharts.com/license
  8. *
  9. * !!!!!!! SOURCE GETS TRANSPILED BY TYPESCRIPT. EDIT TS FILE ONLY. !!!!!!!
  10. *
  11. * */
  12. 'use strict';
  13. /* global MSBlobBuilder */
  14. import Chart from '../Core/Chart/Chart.js';
  15. import H from '../Core/Globals.js';
  16. var win = H.win, doc = H.doc;
  17. import '../Core/Options.js';
  18. import SVGRenderer from '../Core/Renderer/SVG/SVGRenderer.js';
  19. import U from '../Core/Utilities.js';
  20. var addEvent = U.addEvent, error = U.error, extend = U.extend, getOptions = U.getOptions, merge = U.merge;
  21. import DownloadURL from '../Extensions/DownloadURL.js';
  22. var downloadURL = DownloadURL.downloadURL;
  23. var domurl = win.URL || win.webkitURL || win, nav = win.navigator, isMSBrowser = /Edge\/|Trident\/|MSIE /.test(nav.userAgent),
  24. // Milliseconds to defer image load event handlers to offset IE bug
  25. loadEventDeferDelay = isMSBrowser ? 150 : 0;
  26. // Dummy object so we can reuse our canvas-tools.js without errors
  27. H.CanVGRenderer = {};
  28. /* eslint-disable valid-jsdoc */
  29. /**
  30. * Downloads a script and executes a callback when done.
  31. *
  32. * @private
  33. * @function getScript
  34. * @param {string} scriptLocation
  35. * @param {Function} callback
  36. * @return {void}
  37. */
  38. function getScript(scriptLocation, callback) {
  39. var head = doc.getElementsByTagName('head')[0], script = doc.createElement('script');
  40. script.type = 'text/javascript';
  41. script.src = scriptLocation;
  42. script.onload = callback;
  43. script.onerror = function () {
  44. error('Error loading script ' + scriptLocation);
  45. };
  46. head.appendChild(script);
  47. }
  48. /**
  49. * Get blob URL from SVG code. Falls back to normal data URI.
  50. *
  51. * @private
  52. * @function Highcharts.svgToDataURL
  53. * @param {string} svg
  54. * @return {string}
  55. */
  56. H.svgToDataUrl = function (svg) {
  57. // Webkit and not chrome
  58. var webKit = (nav.userAgent.indexOf('WebKit') > -1 &&
  59. nav.userAgent.indexOf('Chrome') < 0);
  60. try {
  61. // Safari requires data URI since it doesn't allow navigation to blob
  62. // URLs. Firefox has an issue with Blobs and internal references,
  63. // leading to gradients not working using Blobs (#4550)
  64. if (!webKit && nav.userAgent.toLowerCase().indexOf('firefox') < 0) {
  65. return domurl.createObjectURL(new win.Blob([svg], {
  66. type: 'image/svg+xml;charset-utf-16'
  67. }));
  68. }
  69. }
  70. catch (e) {
  71. // Ignore
  72. }
  73. return 'data:image/svg+xml;charset=UTF-8,' + encodeURIComponent(svg);
  74. };
  75. /**
  76. * Get data:URL from image URL. Pass in callbacks to handle results.
  77. *
  78. * @private
  79. * @function Highcharts.imageToDataUrl
  80. *
  81. * @param {string} imageURL
  82. *
  83. * @param {string} imageType
  84. *
  85. * @param {*} callbackArgs
  86. * callbackArgs is used only by callbacks.
  87. *
  88. * @param {number} scale
  89. *
  90. * @param {Function} successCallback
  91. * Receives four arguments: imageURL, imageType, callbackArgs, and scale.
  92. *
  93. * @param {Function} taintedCallback
  94. * Receives four arguments: imageURL, imageType, callbackArgs, and scale.
  95. *
  96. * @param {Function} noCanvasSupportCallback
  97. * Receives four arguments: imageURL, imageType, callbackArgs, and scale.
  98. *
  99. * @param {Function} failedLoadCallback
  100. * Receives four arguments: imageURL, imageType, callbackArgs, and scale.
  101. *
  102. * @param {Function} [finallyCallback]
  103. * finallyCallback is always called at the end of the process. All
  104. * callbacks receive four arguments: imageURL, imageType, callbackArgs,
  105. * and scale.
  106. *
  107. * @return {void}
  108. */
  109. H.imageToDataUrl = function (imageURL, imageType, callbackArgs, scale, successCallback, taintedCallback, noCanvasSupportCallback, failedLoadCallback, finallyCallback) {
  110. var img = new win.Image(), taintedHandler, loadHandler = function () {
  111. setTimeout(function () {
  112. var canvas = doc.createElement('canvas'), ctx = canvas.getContext && canvas.getContext('2d'), dataURL;
  113. try {
  114. if (!ctx) {
  115. noCanvasSupportCallback(imageURL, imageType, callbackArgs, scale);
  116. }
  117. else {
  118. canvas.height = img.height * scale;
  119. canvas.width = img.width * scale;
  120. ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
  121. // Now we try to get the contents of the canvas.
  122. try {
  123. dataURL = canvas.toDataURL(imageType);
  124. successCallback(dataURL, imageType, callbackArgs, scale);
  125. }
  126. catch (e) {
  127. taintedHandler(imageURL, imageType, callbackArgs, scale);
  128. }
  129. }
  130. }
  131. finally {
  132. if (finallyCallback) {
  133. finallyCallback(imageURL, imageType, callbackArgs, scale);
  134. }
  135. }
  136. // IE bug where image is not always ready despite calling load
  137. // event.
  138. }, loadEventDeferDelay);
  139. },
  140. // Image load failed (e.g. invalid URL)
  141. errorHandler = function () {
  142. failedLoadCallback(imageURL, imageType, callbackArgs, scale);
  143. if (finallyCallback) {
  144. finallyCallback(imageURL, imageType, callbackArgs, scale);
  145. }
  146. };
  147. // This is called on load if the image drawing to canvas failed with a
  148. // security error. We retry the drawing with crossOrigin set to Anonymous.
  149. taintedHandler = function () {
  150. img = new win.Image();
  151. taintedHandler = taintedCallback;
  152. // Must be set prior to loading image source
  153. img.crossOrigin = 'Anonymous';
  154. img.onload = loadHandler;
  155. img.onerror = errorHandler;
  156. img.src = imageURL;
  157. };
  158. img.onload = loadHandler;
  159. img.onerror = errorHandler;
  160. img.src = imageURL;
  161. };
  162. /* eslint-enable valid-jsdoc */
  163. /**
  164. * Get data URL to an image of an SVG and call download on it options object:
  165. *
  166. * - **filename:** Name of resulting downloaded file without extension. Default
  167. * is `chart`.
  168. *
  169. * - **type:** File type of resulting download. Default is `image/png`.
  170. *
  171. * - **scale:** Scaling factor of downloaded image compared to source. Default
  172. * is `1`.
  173. *
  174. * - **libURL:** URL pointing to location of dependency scripts to download on
  175. * demand. Default is the exporting.libURL option of the global Highcharts
  176. * options pointing to our server.
  177. *
  178. * @function Highcharts.downloadSVGLocal
  179. *
  180. * @param {string} svg
  181. * The generated SVG
  182. *
  183. * @param {Highcharts.ExportingOptions} options
  184. * The exporting options
  185. *
  186. * @param {Function} failCallback
  187. * The callback function in case of errors
  188. *
  189. * @param {Function} [successCallback]
  190. * The callback function in case of success
  191. *
  192. * @return {void}
  193. */
  194. H.downloadSVGLocal = function (svg, options, failCallback, successCallback) {
  195. var svgurl, blob, objectURLRevoke = true, finallyHandler, libURL = (options.libURL || getOptions().exporting.libURL), dummySVGContainer = doc.createElement('div'), imageType = options.type || 'image/png', filename = ((options.filename || 'chart') +
  196. '.' +
  197. (imageType === 'image/svg+xml' ? 'svg' : imageType.split('/')[1])), scale = options.scale || 1;
  198. // Allow libURL to end with or without fordward slash
  199. libURL = libURL.slice(-1) !== '/' ? libURL + '/' : libURL;
  200. /* eslint-disable valid-jsdoc */
  201. /**
  202. * @private
  203. */
  204. function svgToPdf(svgElement, margin) {
  205. var width = svgElement.width.baseVal.value + 2 * margin, height = svgElement.height.baseVal.value + 2 * margin, pdf = new win.jsPDF(// eslint-disable-line new-cap
  206. height > width ? 'p' : 'l', // setting orientation to portrait if height exceeds width
  207. 'pt', [width, height]);
  208. // Workaround for #7090, hidden elements were drawn anyway. It comes
  209. // down to https://github.com/yWorks/svg2pdf.js/issues/28. Check this
  210. // later.
  211. [].forEach.call(svgElement.querySelectorAll('*[visibility="hidden"]'), function (node) {
  212. node.parentNode.removeChild(node);
  213. });
  214. win.svg2pdf(svgElement, pdf, { removeInvalid: true });
  215. return pdf.output('datauristring');
  216. }
  217. /**
  218. * @private
  219. * @return {void}
  220. */
  221. function downloadPDF() {
  222. dummySVGContainer.innerHTML = svg;
  223. var textElements = dummySVGContainer.getElementsByTagName('text'), titleElements, svgData,
  224. // Copy style property to element from parents if it's not there.
  225. // Searches up hierarchy until it finds prop, or hits the chart
  226. // container.
  227. setStylePropertyFromParents = function (el, propName) {
  228. var curParent = el;
  229. while (curParent && curParent !== dummySVGContainer) {
  230. if (curParent.style[propName]) {
  231. el.style[propName] =
  232. curParent.style[propName];
  233. break;
  234. }
  235. curParent = curParent.parentNode;
  236. }
  237. };
  238. // Workaround for the text styling. Making sure it does pick up settings
  239. // for parent elements.
  240. [].forEach.call(textElements, function (el) {
  241. // Workaround for the text styling. making sure it does pick up the
  242. // root element
  243. ['font-family', 'font-size'].forEach(function (property) {
  244. setStylePropertyFromParents(el, property);
  245. });
  246. el.style['font-family'] = (el.style['font-family'] &&
  247. el.style['font-family'].split(' ').splice(-1));
  248. // Workaround for plotband with width, removing title from text
  249. // nodes
  250. titleElements = el.getElementsByTagName('title');
  251. [].forEach.call(titleElements, function (titleElement) {
  252. el.removeChild(titleElement);
  253. });
  254. });
  255. svgData = svgToPdf(dummySVGContainer.firstChild, 0);
  256. try {
  257. downloadURL(svgData, filename);
  258. if (successCallback) {
  259. successCallback();
  260. }
  261. }
  262. catch (e) {
  263. failCallback(e);
  264. }
  265. }
  266. /* eslint-enable valid-jsdoc */
  267. // Initiate download depending on file type
  268. if (imageType === 'image/svg+xml') {
  269. // SVG download. In this case, we want to use Microsoft specific Blob if
  270. // available
  271. try {
  272. if (typeof nav.msSaveOrOpenBlob !== 'undefined') {
  273. blob = new MSBlobBuilder();
  274. blob.append(svg);
  275. svgurl = blob.getBlob('image/svg+xml');
  276. }
  277. else {
  278. svgurl = H.svgToDataUrl(svg);
  279. }
  280. downloadURL(svgurl, filename);
  281. if (successCallback) {
  282. successCallback();
  283. }
  284. }
  285. catch (e) {
  286. failCallback(e);
  287. }
  288. }
  289. else if (imageType === 'application/pdf') {
  290. if (win.jsPDF && win.svg2pdf) {
  291. downloadPDF();
  292. }
  293. else {
  294. // Must load pdf libraries first. // Don't destroy the object URL
  295. // yet since we are doing things asynchronously. A cleaner solution
  296. // would be nice, but this will do for now.
  297. objectURLRevoke = true;
  298. getScript(libURL + 'jspdf.js', function () {
  299. getScript(libURL + 'svg2pdf.js', function () {
  300. downloadPDF();
  301. });
  302. });
  303. }
  304. }
  305. else {
  306. // PNG/JPEG download - create bitmap from SVG
  307. svgurl = H.svgToDataUrl(svg);
  308. finallyHandler = function () {
  309. try {
  310. domurl.revokeObjectURL(svgurl);
  311. }
  312. catch (e) {
  313. // Ignore
  314. }
  315. };
  316. // First, try to get PNG by rendering on canvas
  317. H.imageToDataUrl(svgurl, imageType, {}, scale, function (imageURL) {
  318. // Success
  319. try {
  320. downloadURL(imageURL, filename);
  321. if (successCallback) {
  322. successCallback();
  323. }
  324. }
  325. catch (e) {
  326. failCallback(e);
  327. }
  328. }, function () {
  329. // Failed due to tainted canvas
  330. // Create new and untainted canvas
  331. var canvas = doc.createElement('canvas'), ctx = canvas.getContext('2d'), imageWidth = svg.match(/^<svg[^>]*width\s*=\s*\"?(\d+)\"?[^>]*>/)[1] * scale, imageHeight = svg.match(/^<svg[^>]*height\s*=\s*\"?(\d+)\"?[^>]*>/)[1] * scale, downloadWithCanVG = function () {
  332. ctx.drawSvg(svg, 0, 0, imageWidth, imageHeight);
  333. try {
  334. downloadURL(nav.msSaveOrOpenBlob ?
  335. canvas.msToBlob() :
  336. canvas.toDataURL(imageType), filename);
  337. if (successCallback) {
  338. successCallback();
  339. }
  340. }
  341. catch (e) {
  342. failCallback(e);
  343. }
  344. finally {
  345. finallyHandler();
  346. }
  347. };
  348. canvas.width = imageWidth;
  349. canvas.height = imageHeight;
  350. if (win.canvg) {
  351. // Use preloaded canvg
  352. downloadWithCanVG();
  353. }
  354. else {
  355. // Must load canVG first. // Don't destroy the object URL
  356. // yet since we are doing things asynchronously. A cleaner
  357. // solution would be nice, but this will do for now.
  358. objectURLRevoke = true;
  359. // Get RGBColor.js first, then canvg
  360. getScript(libURL + 'rgbcolor.js', function () {
  361. getScript(libURL + 'canvg.js', function () {
  362. downloadWithCanVG();
  363. });
  364. });
  365. }
  366. },
  367. // No canvas support
  368. failCallback,
  369. // Failed to load image
  370. failCallback,
  371. // Finally
  372. function () {
  373. if (objectURLRevoke) {
  374. finallyHandler();
  375. }
  376. });
  377. }
  378. };
  379. /* eslint-disable valid-jsdoc */
  380. /**
  381. * Get SVG of chart prepared for client side export. This converts embedded
  382. * images in the SVG to data URIs. It requires the regular exporting module. The
  383. * options and chartOptions arguments are passed to the getSVGForExport
  384. * function.
  385. *
  386. * @private
  387. * @function Highcharts.Chart#getSVGForLocalExport
  388. * @param {Highcharts.ExportingOptions} options
  389. * @param {Highcharts.Options} chartOptions
  390. * @param {Function} failCallback
  391. * @param {Function} successCallback
  392. * @return {void}
  393. */
  394. Chart.prototype.getSVGForLocalExport = function (options, chartOptions, failCallback, successCallback) {
  395. var chart = this, images, imagesEmbedded = 0, chartCopyContainer, chartCopyOptions, el, i, l, href,
  396. // After grabbing the SVG of the chart's copy container we need to do
  397. // sanitation on the SVG
  398. sanitize = function (svg) {
  399. return chart.sanitizeSVG(svg, chartCopyOptions);
  400. },
  401. // When done with last image we have our SVG
  402. checkDone = function () {
  403. if (imagesEmbedded === images.length) {
  404. successCallback(sanitize(chartCopyContainer.innerHTML));
  405. }
  406. },
  407. // Success handler, we converted image to base64!
  408. embeddedSuccess = function (imageURL, imageType, callbackArgs) {
  409. ++imagesEmbedded;
  410. // Change image href in chart copy
  411. callbackArgs.imageElement.setAttributeNS('http://www.w3.org/1999/xlink', 'href', imageURL);
  412. checkDone();
  413. };
  414. // Hook into getSVG to get a copy of the chart copy's container (#8273)
  415. chart.unbindGetSVG = addEvent(chart, 'getSVG', function (e) {
  416. chartCopyOptions = e.chartCopy.options;
  417. chartCopyContainer = e.chartCopy.container.cloneNode(true);
  418. });
  419. // Trigger hook to get chart copy
  420. chart.getSVGForExport(options, chartOptions);
  421. images = chartCopyContainer.getElementsByTagName('image');
  422. try {
  423. // If there are no images to embed, the SVG is okay now.
  424. if (!images.length) {
  425. // Use SVG of chart copy
  426. successCallback(sanitize(chartCopyContainer.innerHTML));
  427. return;
  428. }
  429. // Go through the images we want to embed
  430. for (i = 0, l = images.length; i < l; ++i) {
  431. el = images[i];
  432. href = el.getAttributeNS('http://www.w3.org/1999/xlink', 'href');
  433. if (href) {
  434. H.imageToDataUrl(href, 'image/png', { imageElement: el }, options.scale, embeddedSuccess,
  435. // Tainted canvas
  436. failCallback,
  437. // No canvas support
  438. failCallback,
  439. // Failed to load source
  440. failCallback);
  441. // Hidden, boosted series have blank href (#10243)
  442. }
  443. else {
  444. ++imagesEmbedded;
  445. el.parentNode.removeChild(el);
  446. checkDone();
  447. }
  448. }
  449. }
  450. catch (e) {
  451. failCallback(e);
  452. }
  453. // Clean up
  454. chart.unbindGetSVG();
  455. };
  456. /* eslint-enable valid-jsdoc */
  457. /**
  458. * Exporting and offline-exporting modules required. Export a chart to an image
  459. * locally in the user's browser.
  460. *
  461. * @function Highcharts.Chart#exportChartLocal
  462. *
  463. * @param {Highcharts.ExportingOptions} [exportingOptions]
  464. * Exporting options, the same as in
  465. * {@link Highcharts.Chart#exportChart}.
  466. *
  467. * @param {Highcharts.Options} [chartOptions]
  468. * Additional chart options for the exported chart. For example a
  469. * different background color can be added here, or `dataLabels`
  470. * for export only.
  471. *
  472. * @return {void}
  473. *
  474. * @requires modules/exporting
  475. */
  476. Chart.prototype.exportChartLocal = function (exportingOptions, chartOptions) {
  477. var chart = this, options = merge(chart.options.exporting, exportingOptions), fallbackToExportServer = function (err) {
  478. if (options.fallbackToExportServer === false) {
  479. if (options.error) {
  480. options.error(options, err);
  481. }
  482. else {
  483. error(28, true); // Fallback disabled
  484. }
  485. }
  486. else {
  487. chart.exportChart(options);
  488. }
  489. }, svgSuccess = function (svg) {
  490. // If SVG contains foreignObjects all exports except SVG will fail,
  491. // as both CanVG and svg2pdf choke on this. Gracefully fall back.
  492. if (svg.indexOf('<foreignObject') > -1 &&
  493. options.type !== 'image/svg+xml') {
  494. fallbackToExportServer('Image type not supported' +
  495. 'for charts with embedded HTML');
  496. }
  497. else {
  498. H.downloadSVGLocal(svg, extend({ filename: chart.getFilename() }, options), fallbackToExportServer);
  499. }
  500. },
  501. // Return true if the SVG contains images with external data. With the
  502. // boost module there are `image` elements with encoded PNGs, these are
  503. // supported by svg2pdf and should pass (#10243).
  504. hasExternalImages = function () {
  505. return [].some.call(chart.container.getElementsByTagName('image'), function (image) {
  506. var href = image.getAttribute('href');
  507. return href !== '' && href.indexOf('data:') !== 0;
  508. });
  509. };
  510. // If we are on IE and in styled mode, add a whitelist to the renderer for
  511. // inline styles that we want to pass through. There are so many styles by
  512. // default in IE that we don't want to blacklist them all.
  513. if (isMSBrowser && chart.styledMode) {
  514. SVGRenderer.prototype.inlineWhitelist = [
  515. /^blockSize/,
  516. /^border/,
  517. /^caretColor/,
  518. /^color/,
  519. /^columnRule/,
  520. /^columnRuleColor/,
  521. /^cssFloat/,
  522. /^cursor/,
  523. /^fill$/,
  524. /^fillOpacity/,
  525. /^font/,
  526. /^inlineSize/,
  527. /^length/,
  528. /^lineHeight/,
  529. /^opacity/,
  530. /^outline/,
  531. /^parentRule/,
  532. /^rx$/,
  533. /^ry$/,
  534. /^stroke/,
  535. /^textAlign/,
  536. /^textAnchor/,
  537. /^textDecoration/,
  538. /^transform/,
  539. /^vectorEffect/,
  540. /^visibility/,
  541. /^x$/,
  542. /^y$/
  543. ];
  544. }
  545. // Always fall back on:
  546. // - MS browsers: Embedded images JPEG/PNG, or any PDF
  547. // - Embedded images and PDF
  548. if ((isMSBrowser &&
  549. (options.type === 'application/pdf' ||
  550. chart.container.getElementsByTagName('image').length &&
  551. options.type !== 'image/svg+xml')) || (options.type === 'application/pdf' &&
  552. hasExternalImages())) {
  553. fallbackToExportServer('Image type not supported for this chart/browser.');
  554. return;
  555. }
  556. chart.getSVGForLocalExport(options, chartOptions, fallbackToExportServer, svgSuccess);
  557. };
  558. // Extend the default options to use the local exporter logic
  559. merge(true, getOptions().exporting, {
  560. libURL: 'https://code.highcharts.com/8.2.0/lib/',
  561. // When offline-exporting is loaded, redefine the menu item definitions
  562. // related to download.
  563. menuItemDefinitions: {
  564. downloadPNG: {
  565. textKey: 'downloadPNG',
  566. onclick: function () {
  567. this.exportChartLocal();
  568. }
  569. },
  570. downloadJPEG: {
  571. textKey: 'downloadJPEG',
  572. onclick: function () {
  573. this.exportChartLocal({
  574. type: 'image/jpeg'
  575. });
  576. }
  577. },
  578. downloadSVG: {
  579. textKey: 'downloadSVG',
  580. onclick: function () {
  581. this.exportChartLocal({
  582. type: 'image/svg+xml'
  583. });
  584. }
  585. },
  586. downloadPDF: {
  587. textKey: 'downloadPDF',
  588. onclick: function () {
  589. this.exportChartLocal({
  590. type: 'application/pdf'
  591. });
  592. }
  593. }
  594. }
  595. });