Skip to content

Commit 7e789b3

Browse files
authored
Merge pull request #4008 from plotly/createObjectURL
Use URL.createObjectURL during Plotly.downloadImage
2 parents 1708fa0 + ecf3ac7 commit 7e789b3

File tree

7 files changed

+162
-65
lines changed

7 files changed

+162
-65
lines changed

src/lib/index.js

+10
Original file line numberDiff line numberDiff line change
@@ -733,6 +733,16 @@ lib.isIE = function() {
733733
return typeof window.navigator.msSaveBlob !== 'undefined';
734734
};
735735

736+
var IS_IE9_OR_BELOW_REGEX = /MSIE [1-9]\./;
737+
lib.isIE9orBelow = function() {
738+
return lib.isIE() && IS_IE9_OR_BELOW_REGEX.test(window.navigator.userAgent);
739+
};
740+
741+
var IS_SAFARI_REGEX = /Version\/[\d\.]+.*Safari/;
742+
lib.isSafari = function() {
743+
return IS_SAFARI_REGEX.test(window.navigator.userAgent);
744+
};
745+
736746
/**
737747
* Duck typing to recognize a d3 selection, mostly for IE9's benefit
738748
* because it doesn't handle instanceof like modern browsers

src/plot_api/to_image.js

+2-4
Original file line numberDiff line numberDiff line change
@@ -74,8 +74,6 @@ var attrs = {
7474
}
7575
};
7676

77-
var IMAGE_URL_PREFIX = /^data:image\/\w+;base64,/;
78-
7977
/** Plotly.toImage
8078
*
8179
* @param {object | string | HTML div} gd
@@ -179,7 +177,7 @@ function toImage(gd, opts) {
179177
if(imageDataOnly) {
180178
return resolve(svg);
181179
} else {
182-
return resolve('data:image/svg+xml,' + encodeURIComponent(svg));
180+
return resolve(helpers.encodeSVG(svg));
183181
}
184182
}
185183

@@ -206,7 +204,7 @@ function toImage(gd, opts) {
206204

207205
function urlToImageData(url) {
208206
if(imageDataOnly) {
209-
return url.replace(IMAGE_URL_PREFIX, '');
207+
return url.replace(helpers.IMAGE_URL_PREFIX, '');
210208
} else {
211209
return url;
212210
}

src/snapshot/download.js

+10-7
Original file line numberDiff line numberDiff line change
@@ -8,27 +8,30 @@
88

99
'use strict';
1010

11-
var toImage = require('../plot_api/to_image');
1211
var Lib = require('../lib');
12+
13+
var toImage = require('../plot_api/to_image');
14+
1315
var fileSaver = require('./filesaver');
16+
var helpers = require('./helpers');
1417

15-
/** Plotly.downloadImage
18+
/**
19+
* Plotly.downloadImage
1620
*
1721
* @param {object | string | HTML div} gd
1822
* can either be a data/layout/config object
1923
* or an existing graph <div>
2024
* or an id to an existing graph <div>
21-
* @param {object} opts (see ../plot_api/to_image)
25+
* @param {object} opts (see Plotly.toImage in ../plot_api/to_image)
2226
* @return {promise}
2327
*/
2428
function downloadImage(gd, opts) {
2529
var _gd;
2630
if(!Lib.isPlainObject(gd)) _gd = Lib.getGraphDiv(gd);
2731

28-
// check for undefined opts
2932
opts = opts || {};
30-
// default to png
3133
opts.format = opts.format || 'png';
34+
opts.imageDataOnly = true;
3235

3336
return new Promise(function(resolve, reject) {
3437
if(_gd && _gd._snapshotInProgress) {
@@ -41,7 +44,7 @@ function downloadImage(gd, opts) {
4144
// does not allow toDataURL
4245
// svg format will work though
4346
if(Lib.isIE() && opts.format !== 'svg') {
44-
reject(new Error('Sorry IE does not support downloading from canvas. Try {format:\'svg\'} instead.'));
47+
reject(new Error(helpers.MSG_IE_BAD_FORMAT));
4548
}
4649

4750
if(_gd) _gd._snapshotInProgress = true;
@@ -52,7 +55,7 @@ function downloadImage(gd, opts) {
5255

5356
promise.then(function(result) {
5457
if(_gd) _gd._snapshotInProgress = false;
55-
return fileSaver(result, filename);
58+
return fileSaver(result, filename, opts.format);
5659
}).then(function(name) {
5760
resolve(name);
5861
}).catch(function(err) {

src/snapshot/filesaver.js

+34-26
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,11 @@
66
* LICENSE file in the root directory of this source tree.
77
*/
88

9+
'use strict';
10+
11+
var Lib = require('../lib');
12+
var helpers = require('./helpers');
13+
914
/*
1015
* substantial portions of this code from FileSaver.js
1116
* https://github.com/eligrey/FileSaver.js
@@ -18,53 +23,56 @@
1823
* License: MIT
1924
* See https://github.com/eligrey/FileSaver.js/blob/master/LICENSE.md
2025
*/
21-
22-
'use strict';
23-
24-
var fileSaver = function(url, name) {
26+
function fileSaver(url, name, format) {
2527
var saveLink = document.createElement('a');
2628
var canUseSaveLink = 'download' in saveLink;
27-
var isSafari = /Version\/[\d\.]+.*Safari/.test(navigator.userAgent);
29+
2830
var promise = new Promise(function(resolve, reject) {
29-
// IE <10 is explicitly unsupported
30-
if(typeof navigator !== 'undefined' && /MSIE [1-9]\./.test(navigator.userAgent)) {
31+
var blob;
32+
var objectUrl;
33+
34+
if(Lib.isIE9orBelow()) {
3135
reject(new Error('IE < 10 unsupported'));
3236
}
3337

34-
// First try a.download, then web filesystem, then object URLs
35-
if(isSafari) {
36-
// Safari doesn't allow downloading of blob urls
37-
document.location.href = 'data:application/octet-stream' + url.slice(url.search(/[,;]/));
38-
resolve(name);
38+
// Safari doesn't allow downloading of blob urls
39+
if(Lib.isSafari()) {
40+
var prefix = format === 'svg' ? ',' : ';base64,';
41+
helpers.octetStream(prefix + encodeURIComponent(url));
42+
return resolve(name);
3943
}
4044

41-
if(!name) {
42-
name = 'download';
45+
// IE 10+ (native saveAs)
46+
if(Lib.isIE()) {
47+
// At this point we are only dealing with a decoded SVG as
48+
// a data URL (since IE only supports SVG)
49+
blob = helpers.createBlob(url, 'svg');
50+
window.navigator.msSaveBlob(blob, name);
51+
blob = null;
52+
return resolve(name);
4353
}
4454

4555
if(canUseSaveLink) {
46-
saveLink.href = url;
56+
blob = helpers.createBlob(url, format);
57+
objectUrl = helpers.createObjectURL(blob);
58+
59+
saveLink.href = objectUrl;
4760
saveLink.download = name;
4861
document.body.appendChild(saveLink);
4962
saveLink.click();
63+
5064
document.body.removeChild(saveLink);
51-
resolve(name);
52-
}
65+
helpers.revokeObjectURL(objectUrl);
66+
blob = null;
5367

54-
// IE 10+ (native saveAs)
55-
if(typeof navigator !== 'undefined' && navigator.msSaveBlob) {
56-
// At this point we are only dealing with a SVG encoded as
57-
// a data URL (since IE only supports SVG)
58-
var encoded = url.split(/^data:image\/svg\+xml,/)[1];
59-
var svg = decodeURIComponent(encoded);
60-
navigator.msSaveBlob(new Blob([svg]), name);
61-
resolve(name);
68+
return resolve(name);
6269
}
6370

6471
reject(new Error('download error'));
6572
});
6673

6774
return promise;
68-
};
75+
}
76+
6977

7078
module.exports = fileSaver;

src/snapshot/helpers.js

+42
Original file line numberDiff line numberDiff line change
@@ -31,3 +31,45 @@ exports.getRedrawFunc = function(gd) {
3131
}
3232
};
3333
};
34+
35+
exports.encodeSVG = function(svg) {
36+
return 'data:image/svg+xml,' + encodeURIComponent(svg);
37+
};
38+
39+
var DOM_URL = window.URL || window.webkitURL;
40+
41+
exports.createObjectURL = function(blob) {
42+
return DOM_URL.createObjectURL(blob);
43+
};
44+
45+
exports.revokeObjectURL = function(url) {
46+
return DOM_URL.revokeObjectURL(url);
47+
};
48+
49+
exports.createBlob = function(url, format) {
50+
if(format === 'svg') {
51+
return new window.Blob([url], {type: 'image/svg+xml;charset=utf-8'});
52+
} else {
53+
var binary = fixBinary(window.atob(url));
54+
return new window.Blob([binary], {type: 'image/' + format});
55+
}
56+
};
57+
58+
exports.octetStream = function(s) {
59+
document.location.href = 'data:application/octet-stream' + s;
60+
};
61+
62+
// Taken from https://bl.ocks.org/nolanlawson/0eac306e4dac2114c752
63+
function fixBinary(b) {
64+
var len = b.length;
65+
var buf = new ArrayBuffer(len);
66+
var arr = new Uint8Array(buf);
67+
for(var i = 0; i < len; i++) {
68+
arr[i] = b.charCodeAt(i);
69+
}
70+
return buf;
71+
}
72+
73+
exports.IMAGE_URL_PREFIX = /^data:image\/\w+;base64,/;
74+
75+
exports.MSG_IE_BAD_FORMAT = 'Sorry IE does not support downloading from canvas. Try {format:\'svg\'} instead.';

src/snapshot/svgtoimg.js

+16-5
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@
1111
var Lib = require('../lib');
1212
var EventEmitter = require('events').EventEmitter;
1313

14+
var helpers = require('./helpers');
15+
1416
function svgToImg(opts) {
1517
var ev = opts.emitter || new EventEmitter();
1618

@@ -21,7 +23,7 @@ function svgToImg(opts) {
2123

2224
// IE only support svg
2325
if(Lib.isIE() && format !== 'svg') {
24-
var ieSvgError = new Error('Sorry IE does not support downloading from canvas. Try {format:\'svg\'} instead.');
26+
var ieSvgError = new Error(helpers.MSG_IE_BAD_FORMAT);
2527
reject(ieSvgError);
2628
// eventually remove the ev
2729
// in favor of promises
@@ -41,18 +43,24 @@ function svgToImg(opts) {
4143

4244
var ctx = canvas.getContext('2d');
4345
var img = new Image();
46+
var svgBlob, url;
4447

45-
// for Safari support, eliminate createObjectURL
46-
// this decision could cause problems if content
47-
// is not restricted to svg
48-
var url = 'data:image/svg+xml,' + encodeURIComponent(svg);
48+
if(format === 'svg' || Lib.isIE9orBelow() || Lib.isSafari()) {
49+
url = helpers.encodeSVG(svg);
50+
} else {
51+
svgBlob = helpers.createBlob(svg, 'svg');
52+
url = helpers.createObjectURL(svgBlob);
53+
}
4954

5055
canvas.width = w1;
5156
canvas.height = h1;
5257

5358
img.onload = function() {
5459
var imgData;
5560

61+
svgBlob = null;
62+
helpers.revokeObjectURL(url);
63+
5664
// don't need to draw to canvas if svg
5765
// save some time and also avoid failure on IE
5866
if(format !== 'svg') {
@@ -90,6 +98,9 @@ function svgToImg(opts) {
9098
};
9199

92100
img.onerror = function(err) {
101+
svgBlob = null;
102+
helpers.revokeObjectURL(url);
103+
93104
reject(err);
94105
// eventually remove the ev
95106
// in favor of promises

0 commit comments

Comments
 (0)