offline-exporting.src.js 22 KB

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