diff --git a/src/lib/index.js b/src/lib/index.js index 639360bb294..a4475bf3aee 100644 --- a/src/lib/index.js +++ b/src/lib/index.js @@ -733,6 +733,16 @@ lib.isIE = function() { return typeof window.navigator.msSaveBlob !== 'undefined'; }; +var IS_IE9_OR_BELOW_REGEX = /MSIE [1-9]\./; +lib.isIE9orBelow = function() { + return lib.isIE() && IS_IE9_OR_BELOW_REGEX.test(window.navigator.userAgent); +}; + +var IS_SAFARI_REGEX = /Version\/[\d\.]+.*Safari/; +lib.isSafari = function() { + return IS_SAFARI_REGEX.test(window.navigator.userAgent); +}; + /** * Duck typing to recognize a d3 selection, mostly for IE9's benefit * because it doesn't handle instanceof like modern browsers diff --git a/src/plot_api/to_image.js b/src/plot_api/to_image.js index c1a8c8733e1..1c5890ce528 100644 --- a/src/plot_api/to_image.js +++ b/src/plot_api/to_image.js @@ -74,8 +74,6 @@ var attrs = { } }; -var IMAGE_URL_PREFIX = /^data:image\/\w+;base64,/; - /** Plotly.toImage * * @param {object | string | HTML div} gd @@ -179,7 +177,7 @@ function toImage(gd, opts) { if(imageDataOnly) { return resolve(svg); } else { - return resolve('data:image/svg+xml,' + encodeURIComponent(svg)); + return resolve(helpers.encodeSVG(svg)); } } @@ -206,7 +204,7 @@ function toImage(gd, opts) { function urlToImageData(url) { if(imageDataOnly) { - return url.replace(IMAGE_URL_PREFIX, ''); + return url.replace(helpers.IMAGE_URL_PREFIX, ''); } else { return url; } diff --git a/src/snapshot/download.js b/src/snapshot/download.js index 64c3302a159..55c9187319d 100644 --- a/src/snapshot/download.js +++ b/src/snapshot/download.js @@ -8,27 +8,30 @@ 'use strict'; -var toImage = require('../plot_api/to_image'); var Lib = require('../lib'); + +var toImage = require('../plot_api/to_image'); + var fileSaver = require('./filesaver'); +var helpers = require('./helpers'); -/** Plotly.downloadImage +/** + * Plotly.downloadImage * * @param {object | string | HTML div} gd * can either be a data/layout/config object * or an existing graph
* or an id to an existing graph
- * @param {object} opts (see ../plot_api/to_image) + * @param {object} opts (see Plotly.toImage in ../plot_api/to_image) * @return {promise} */ function downloadImage(gd, opts) { var _gd; if(!Lib.isPlainObject(gd)) _gd = Lib.getGraphDiv(gd); - // check for undefined opts opts = opts || {}; - // default to png opts.format = opts.format || 'png'; + opts.imageDataOnly = true; return new Promise(function(resolve, reject) { if(_gd && _gd._snapshotInProgress) { @@ -41,7 +44,7 @@ function downloadImage(gd, opts) { // does not allow toDataURL // svg format will work though if(Lib.isIE() && opts.format !== 'svg') { - reject(new Error('Sorry IE does not support downloading from canvas. Try {format:\'svg\'} instead.')); + reject(new Error(helpers.MSG_IE_BAD_FORMAT)); } if(_gd) _gd._snapshotInProgress = true; @@ -52,7 +55,7 @@ function downloadImage(gd, opts) { promise.then(function(result) { if(_gd) _gd._snapshotInProgress = false; - return fileSaver(result, filename); + return fileSaver(result, filename, opts.format); }).then(function(name) { resolve(name); }).catch(function(err) { diff --git a/src/snapshot/filesaver.js b/src/snapshot/filesaver.js index e4132a1aa04..00e6435670a 100644 --- a/src/snapshot/filesaver.js +++ b/src/snapshot/filesaver.js @@ -6,6 +6,11 @@ * LICENSE file in the root directory of this source tree. */ +'use strict'; + +var Lib = require('../lib'); +var helpers = require('./helpers'); + /* * substantial portions of this code from FileSaver.js * https://github.com/eligrey/FileSaver.js @@ -18,53 +23,56 @@ * License: MIT * See https://github.com/eligrey/FileSaver.js/blob/master/LICENSE.md */ - -'use strict'; - -var fileSaver = function(url, name) { +function fileSaver(url, name, format) { var saveLink = document.createElement('a'); var canUseSaveLink = 'download' in saveLink; - var isSafari = /Version\/[\d\.]+.*Safari/.test(navigator.userAgent); + var promise = new Promise(function(resolve, reject) { - // IE <10 is explicitly unsupported - if(typeof navigator !== 'undefined' && /MSIE [1-9]\./.test(navigator.userAgent)) { + var blob; + var objectUrl; + + if(Lib.isIE9orBelow()) { reject(new Error('IE < 10 unsupported')); } - // First try a.download, then web filesystem, then object URLs - if(isSafari) { - // Safari doesn't allow downloading of blob urls - document.location.href = 'data:application/octet-stream' + url.slice(url.search(/[,;]/)); - resolve(name); + // Safari doesn't allow downloading of blob urls + if(Lib.isSafari()) { + var prefix = format === 'svg' ? ',' : ';base64,'; + helpers.octetStream(prefix + encodeURIComponent(url)); + return resolve(name); } - if(!name) { - name = 'download'; + // IE 10+ (native saveAs) + if(Lib.isIE()) { + // At this point we are only dealing with a decoded SVG as + // a data URL (since IE only supports SVG) + blob = helpers.createBlob(url, 'svg'); + window.navigator.msSaveBlob(blob, name); + blob = null; + return resolve(name); } if(canUseSaveLink) { - saveLink.href = url; + blob = helpers.createBlob(url, format); + objectUrl = helpers.createObjectURL(blob); + + saveLink.href = objectUrl; saveLink.download = name; document.body.appendChild(saveLink); saveLink.click(); + document.body.removeChild(saveLink); - resolve(name); - } + helpers.revokeObjectURL(objectUrl); + blob = null; - // IE 10+ (native saveAs) - if(typeof navigator !== 'undefined' && navigator.msSaveBlob) { - // At this point we are only dealing with a SVG encoded as - // a data URL (since IE only supports SVG) - var encoded = url.split(/^data:image\/svg\+xml,/)[1]; - var svg = decodeURIComponent(encoded); - navigator.msSaveBlob(new Blob([svg]), name); - resolve(name); + return resolve(name); } reject(new Error('download error')); }); return promise; -}; +} + module.exports = fileSaver; diff --git a/src/snapshot/helpers.js b/src/snapshot/helpers.js index 8b10678872b..47672dfdc00 100644 --- a/src/snapshot/helpers.js +++ b/src/snapshot/helpers.js @@ -31,3 +31,45 @@ exports.getRedrawFunc = function(gd) { } }; }; + +exports.encodeSVG = function(svg) { + return 'data:image/svg+xml,' + encodeURIComponent(svg); +}; + +var DOM_URL = window.URL || window.webkitURL; + +exports.createObjectURL = function(blob) { + return DOM_URL.createObjectURL(blob); +}; + +exports.revokeObjectURL = function(url) { + return DOM_URL.revokeObjectURL(url); +}; + +exports.createBlob = function(url, format) { + if(format === 'svg') { + return new window.Blob([url], {type: 'image/svg+xml;charset=utf-8'}); + } else { + var binary = fixBinary(window.atob(url)); + return new window.Blob([binary], {type: 'image/' + format}); + } +}; + +exports.octetStream = function(s) { + document.location.href = 'data:application/octet-stream' + s; +}; + +// Taken from https://bl.ocks.org/nolanlawson/0eac306e4dac2114c752 +function fixBinary(b) { + var len = b.length; + var buf = new ArrayBuffer(len); + var arr = new Uint8Array(buf); + for(var i = 0; i < len; i++) { + arr[i] = b.charCodeAt(i); + } + return buf; +} + +exports.IMAGE_URL_PREFIX = /^data:image\/\w+;base64,/; + +exports.MSG_IE_BAD_FORMAT = 'Sorry IE does not support downloading from canvas. Try {format:\'svg\'} instead.'; diff --git a/src/snapshot/svgtoimg.js b/src/snapshot/svgtoimg.js index d137c57b106..6117f04a3f9 100644 --- a/src/snapshot/svgtoimg.js +++ b/src/snapshot/svgtoimg.js @@ -11,6 +11,8 @@ var Lib = require('../lib'); var EventEmitter = require('events').EventEmitter; +var helpers = require('./helpers'); + function svgToImg(opts) { var ev = opts.emitter || new EventEmitter(); @@ -21,7 +23,7 @@ function svgToImg(opts) { // IE only support svg if(Lib.isIE() && format !== 'svg') { - var ieSvgError = new Error('Sorry IE does not support downloading from canvas. Try {format:\'svg\'} instead.'); + var ieSvgError = new Error(helpers.MSG_IE_BAD_FORMAT); reject(ieSvgError); // eventually remove the ev // in favor of promises @@ -41,11 +43,14 @@ function svgToImg(opts) { var ctx = canvas.getContext('2d'); var img = new Image(); + var svgBlob, url; - // for Safari support, eliminate createObjectURL - // this decision could cause problems if content - // is not restricted to svg - var url = 'data:image/svg+xml,' + encodeURIComponent(svg); + if(format === 'svg' || Lib.isIE9orBelow() || Lib.isSafari()) { + url = helpers.encodeSVG(svg); + } else { + svgBlob = helpers.createBlob(svg, 'svg'); + url = helpers.createObjectURL(svgBlob); + } canvas.width = w1; canvas.height = h1; @@ -53,6 +58,9 @@ function svgToImg(opts) { img.onload = function() { var imgData; + svgBlob = null; + helpers.revokeObjectURL(url); + // don't need to draw to canvas if svg // save some time and also avoid failure on IE if(format !== 'svg') { @@ -90,6 +98,9 @@ function svgToImg(opts) { }; img.onerror = function(err) { + svgBlob = null; + helpers.revokeObjectURL(url); + reject(err); // eventually remove the ev // in favor of promises diff --git a/test/jasmine/tests/download_test.js b/test/jasmine/tests/download_test.js index 23203718160..adee0c911ef 100644 --- a/test/jasmine/tests/download_test.js +++ b/test/jasmine/tests/download_test.js @@ -1,15 +1,16 @@ var Plotly = require('@lib/index'); +var Lib = require('@src/lib'); + +var helpers = require('@src/snapshot/helpers'); + var createGraphDiv = require('../assets/create_graph_div'); var destroyGraphDiv = require('../assets/destroy_graph_div'); -var textchartMock = require('@mocks/text_chart_arrays.json'); var failTest = require('../assets/fail_test'); -var Lib = require('@src/lib'); - +var textchartMock = require('@mocks/text_chart_arrays.json'); var LONG_TIMEOUT_INTERVAL = 2 * jasmine.DEFAULT_TIMEOUT_INTERVAL; describe('Plotly.downloadImage', function() { - 'use strict'; var gd; var createElement = document.createElement; @@ -32,7 +33,7 @@ describe('Plotly.downloadImage', function() { afterEach(function() { destroyGraphDiv(); - delete navigator.msSaveBlob; + delete window.navigator.msSaveBlob; }); it('should be attached to Plotly', function() { @@ -40,17 +41,23 @@ describe('Plotly.downloadImage', function() { }); it('should create link, remove link, accept options', function(done) { - downloadTest(gd, 'jpeg', done); + downloadTest(gd, 'jpeg') + .catch(failTest) + .then(done); }, LONG_TIMEOUT_INTERVAL); it('should create link, remove link, accept options', function(done) { - downloadTest(gd, 'png', done); + downloadTest(gd, 'png') + .catch(failTest) + .then(done); }, LONG_TIMEOUT_INTERVAL); it('should create link, remove link, accept options', function(done) { checkWebp(function(supported) { if(supported) { - downloadTest(gd, 'webp', done); + downloadTest(gd, 'webp') + .catch(failTest) + .then(done); } else { done(); } @@ -58,11 +65,15 @@ describe('Plotly.downloadImage', function() { }, LONG_TIMEOUT_INTERVAL); it('should create link, remove link, accept options', function(done) { - downloadTest(gd, 'svg', done); + downloadTest(gd, 'svg') + .catch(failTest) + .then(done); }, LONG_TIMEOUT_INTERVAL); it('should work when passing graph div id', function(done) { - downloadTest('graph', 'svg', done); + downloadTest('graph', 'svg') + .catch(failTest) + .then(done); }, LONG_TIMEOUT_INTERVAL); it('should work when passing a figure object', function(done) { @@ -87,7 +98,7 @@ describe('Plotly.downloadImage', function() { .replace(/(\(#)([^")]*)(\))/gi, '(\"#$2\")'); }); var savedBlob; - navigator.msSaveBlob = function(blob) { savedBlob = blob; }; + window.navigator.msSaveBlob = function(blob) { savedBlob = blob; }; var expectedStart = '