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 = '