offline-exporting.src.js 22 KB

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