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 all 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
81 changes: 65 additions & 16 deletions test/jasmine/tests/download_test.js
Expand Up @@ -2,36 +2,37 @@ 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;

describe('Plotly.downloadImage', function() {
'use strict';
var gd;

// override click handler on createElement
// so these tests will not actually
// download an image each time they are run
// full credit goes to @etpinard; thanks
var createElement = document.createElement;
beforeAll(function() {
document.createElement = function(args) {
var el = createElement.call(document, args);
el.click = function() {};
return el;
};
});

afterAll(function() {
document.createElement = createElement;
});
var slzProto = (new window.XMLSerializer()).__proto__;
var serializeToString = slzProto.serializeToString;

beforeEach(function() {
gd = createGraphDiv();

// override click handler on createElement
// so these tests will not actually
// download an image each time they are run
// full credit goes to @etpinard; thanks
spyOn(document, 'createElement').and.callFake(function(args) {
var el = createElement.call(document, args);
el.click = function() {};
return el;
});
});

afterEach(function() {
destroyGraphDiv();
delete navigator.msSaveBlob;
});

it('should be attached to Plotly', function() {
Expand Down Expand Up @@ -59,8 +60,56 @@ 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
spyOn(Lib, 'isIE').and.callFake(function() { return true; });
spyOn(slzProto, 'serializeToString').and.callFake(function() {
return serializeToString.apply(this, arguments)
.replace(/(\(#)([^")]*)(\))/gi, '(\"#$2\")');
});
var savedBlob;
navigator.msSaveBlob = function(blob) { savedBlob = blob; };

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() {
if(savedBlob === undefined) {
fail('undefined saveBlob');
}

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