offline-exporting.src.js 23 KB

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