Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

IE/Edge SVG export fixes #2068

Merged
merged 5 commits into from Oct 10, 2017
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
6 changes: 5 additions & 1 deletion src/snapshot/filesaver.js
Expand Up @@ -53,7 +53,11 @@ var fileSaver = function(url, name) {

// IE 10+ (native saveAs)
if(typeof navigator !== 'undefined' && navigator.msSaveBlob) {
navigator.msSaveBlob(new Blob([url]), name);
// 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];
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder what's better (i.e. faster)

url.split(/^data:image\/svg\+xml,/)[1];

// or as in plot_api/to_image.js
url.replace(/^data:image\/svg\+xml,/, '');

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't see perf as too important in this block. An advantage (at least it looks like an advantage to me...) of the current one is that if url does NOT start with the expected 'data:image/svg+xml,' it will throw an error, rather than saving garbage, which seems easier to investigate.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it will throw an error, rather than saving garbage,

Good point. 👍

var svg = decodeURIComponent(encoded);
navigator.msSaveBlob(new Blob([svg]), name);
resolve(name);
}

Expand Down
2 changes: 1 addition & 1 deletion src/snapshot/tosvg.js
Expand Up @@ -165,7 +165,7 @@ module.exports = function toSVG(gd, format, scale) {
// url in svg are single quoted
// since we changed double to single
// we'll need to change these to double-quoted
s = s.replace(/(\('#)([^']*)('\))/gi, '(\"$2\")');
s = s.replace(/(\('#)([^']*)('\))/gi, '(\"#$2\")');
// font names with spaces will be escaped single-quoted
// we'll need to change these to double-quoted
s = s.replace(/(\\')/gi, '\"');
Expand Down
60 changes: 59 additions & 1 deletion test/jasmine/tests/download_test.js
Expand Up @@ -2,6 +2,9 @@ var Plotly = require('@lib/index');
var createGraphDiv = require('../assets/create_graph_div');
var destroyGraphDiv = require('../assets/destroy_graph_div');
var textchartMock = require('@mocks/text_chart_arrays.json');
var fail = require('../assets/fail_test');

var Lib = require('@src/lib');

var LONG_TIMEOUT_INTERVAL = 2 * jasmine.DEFAULT_TIMEOUT_INTERVAL;

Expand All @@ -14,6 +17,11 @@ describe('Plotly.downloadImage', function() {
// download an image each time they are run
// full credit goes to @etpinard; thanks
var createElement = document.createElement;
var msSaveBlob = navigator.msSaveBlob;
var isIE = Lib.isIE;
var slzProto = (new window.XMLSerializer()).__proto__;
var serializeToString = slzProto.serializeToString;

beforeAll(function() {
document.createElement = function(args) {
var el = createElement.call(document, args);
Expand All @@ -32,6 +40,9 @@ describe('Plotly.downloadImage', function() {

afterEach(function() {
destroyGraphDiv();
Lib.isIE = isIE;
slzProto.serializeToString = serializeToString;
navigator.msSaveBlob = msSaveBlob;
});

it('should be attached to Plotly', function() {
Expand Down Expand Up @@ -59,8 +70,55 @@ describe('Plotly.downloadImage', function() {
it('should create link, remove link, accept options', function(done) {
downloadTest(gd, 'svg', done);
}, LONG_TIMEOUT_INTERVAL);
});

it('should produce the right SVG output in IE', function(done) {
// mock up IE behavior
Lib.isIE = function() { return true; };
slzProto.serializeToString = function() {
return serializeToString.apply(this, arguments)
.replace(/(\(#)([^")]*)(\))/gi, '(\"#$2\")');
};
var savedBlob;
navigator.msSaveBlob = function(blob) { savedBlob = blob; };
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be best to use jasmine spyOn and callFake that way we wouldn't have to worry about restoring the unmocked behavior.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ha, you can only spyOn something that already exists - so msSaveBlob I have to handle manually. But Lib.isIE and serializeToString (and perhaps document.createElement??) I can switch to spyOn().and.callFake

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good!


var expectedStart = '<svg class=\'main-svg\' xmlns=\'http://www.w3.org/2000/svg\' xmlns:xlink=\'http://www.w3.org/1999/xlink\'';
var plotClip = /clip-path='url\("#clip[0-9a-f]{6}xyplot"\)/;
var legendClip = /clip-path=\'url\("#legend[0-9a-f]{6}"\)/;

Plotly.plot(gd, textchartMock.data, textchartMock.layout)
.then(function(gd) {
savedBlob = undefined;
return Plotly.downloadImage(gd, {
format: 'svg',
height: 300,
width: 300,
filename: 'plotly_download'
});
})
.then(function() {
expect(savedBlob).toBeDefined();
if(savedBlob === undefined) return;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps

if(saveBlob === undefined) {
  fail('undefined save blob');
}

would be more readable than an expect + early return sequence.


return new Promise(function(resolve, reject) {
var reader = new FileReader();
reader.onloadend = function() {
var res = reader.result;

expect(res.substr(0, expectedStart.length)).toBe(expectedStart);
expect(res.match(plotClip)).not.toBe(null);
expect(res.match(legendClip)).not.toBe(null);

resolve();
};
reader.onerror = function(e) { reject(e); };

reader.readAsText(savedBlob);
});
})
.catch(fail)
.then(done);
}, LONG_TIMEOUT_INTERVAL);
});

function downloadTest(gd, format, done) {
// use MutationObserver to monitor the DOM
Expand Down