From f58c6709c10c645d4c4864f0179732e3c178ba19 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Etienne=20T=C3=A9treault-Pinard?= Date: Wed, 23 Aug 2017 16:09:29 -0400 Subject: [PATCH 1/3] add 'scale' to-image option - which draw image data on bigger canvas (for png/jpeg/webp) - and scales svg using top-level width/height/viewBox attribute --- src/plot_api/to_image.js | 12 +++++++++++- src/snapshot/svgtoimg.js | 12 +++++++++--- src/snapshot/tosvg.js | 20 ++++++++++++++------ 3 files changed, 34 insertions(+), 10 deletions(-) diff --git a/src/plot_api/to_image.js b/src/plot_api/to_image.js index 4ce0821aaaa..77a9ac21101 100644 --- a/src/plot_api/to_image.js +++ b/src/plot_api/to_image.js @@ -40,6 +40,14 @@ var attrs = { 'Defaults to the value found in `layout.height`' ].join(' ') }, + scale: { + valType: 'number', + min: 0, + dflt: 1, + description: [ + '...' + ].join(' ') + }, setBackground: { valType: 'any', dflt: false, @@ -111,6 +119,7 @@ function toImage(gd, opts) { var format = coerce('format'); var width = coerce('width'); var height = coerce('height'); + var scale = coerce('scale'); var setBackground = coerce('setBackground'); var imageDataOnly = coerce('imageDataOnly'); @@ -142,7 +151,7 @@ function toImage(gd, opts) { function convert() { return new Promise(function(resolve, reject) { - var svg = toSVG(clonedGd); + var svg = toSVG(clonedGd, format, scale); var width = clonedGd._fullLayout.width; var height = clonedGd._fullLayout.height; @@ -164,6 +173,7 @@ function toImage(gd, opts) { format: format, width: width, height: height, + scale: scale, canvas: canvas, svg: svg, // ask svgToImg to return a Promise diff --git a/src/snapshot/svgtoimg.js b/src/snapshot/svgtoimg.js index 86310cf5413..8778a7fc776 100644 --- a/src/snapshot/svgtoimg.js +++ b/src/snapshot/svgtoimg.js @@ -33,6 +33,12 @@ function svgToImg(opts) { } var canvas = opts.canvas; + var scale = opts.scale || 1; + var w0 = opts.width || 150; + var h0 = opts.height || 300; + var w1 = scale * w0; + var h1 = scale * h0; + var ctx = canvas.getContext('2d'); var img = new Image(); @@ -41,8 +47,8 @@ function svgToImg(opts) { // is not restricted to svg var url = 'data:image/svg+xml,' + encodeURIComponent(svg); - canvas.height = opts.height || 150; - canvas.width = opts.width || 300; + canvas.width = w1; + canvas.height = h1; img.onload = function() { var imgData; @@ -50,7 +56,7 @@ function svgToImg(opts) { // don't need to draw to canvas if svg // save some time and also avoid failure on IE if(format !== 'svg') { - ctx.drawImage(img, 0, 0); + ctx.drawImage(img, 0, 0, w1, h1); } switch(format) { diff --git a/src/snapshot/tosvg.js b/src/snapshot/tosvg.js index ea7189bedc3..cd536d0edca 100644 --- a/src/snapshot/tosvg.js +++ b/src/snapshot/tosvg.js @@ -36,11 +36,13 @@ function xmlEntityEncode(str) { return str.replace(/&(?!\w+;|\#[0-9]+;| \#x[0-9A-F]+;)/g, '&'); } -module.exports = function toSVG(gd, format) { - var fullLayout = gd._fullLayout, - svg = fullLayout._paper, - toppaper = fullLayout._toppaper, - i; +module.exports = function toSVG(gd, format, scale) { + var fullLayout = gd._fullLayout; + var svg = fullLayout._paper; + var toppaper = fullLayout._toppaper; + var width = fullLayout.width; + var height = fullLayout.height; + var i; // make background color a rect in the svg, then revert after scraping // all other alterations have been dealt with by properly preparing the svg @@ -48,7 +50,7 @@ module.exports = function toSVG(gd, format) { // have to remove them, and providing the right namespaces in the svg to // begin with svg.insert('rect', ':first-child') - .call(Drawing.setRect, 0, 0, fullLayout.width, fullLayout.height) + .call(Drawing.setRect, 0, 0, width, height) .call(Color.fill, fullLayout.paper_bgcolor); // subplot-specific to-SVG methods @@ -137,6 +139,12 @@ module.exports = function toSVG(gd, format) { svg.node().setAttributeNS(xmlnsNamespaces.xmlns, 'xmlns', xmlnsNamespaces.svg); svg.node().setAttributeNS(xmlnsNamespaces.xmlns, 'xmlns:xlink', xmlnsNamespaces.xlink); + if(format === 'svg' && scale) { + svg.attr('width', scale * width); + svg.attr('height', scale * height); + svg.attr('viewBox', '0 0 ' + width + ' ' + height); + } + var s = new window.XMLSerializer().serializeToString(svg.node()); s = htmlEntityDecode(s); s = xmlEntityEncode(s); From 971e3b5919f37f818eaf4c3d1737a4792af83c5f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Etienne=20T=C3=A9treault-Pinard?= Date: Thu, 31 Aug 2017 19:11:14 -0400 Subject: [PATCH 2/3] add toImage *scale* tests and :books: --- src/plot_api/to_image.js | 6 ++++-- test/jasmine/tests/snapshot_test.js | 20 +++++++++++++++++ test/jasmine/tests/toimage_test.js | 33 +++++++++++++++++++++++++++++ 3 files changed, 57 insertions(+), 2 deletions(-) diff --git a/src/plot_api/to_image.js b/src/plot_api/to_image.js index 77a9ac21101..059cd973c50 100644 --- a/src/plot_api/to_image.js +++ b/src/plot_api/to_image.js @@ -45,7 +45,10 @@ var attrs = { min: 0, dflt: 1, description: [ - '...' + 'Sets a scaling for the generated image.', + 'If set, all features of a graphs (e.g. text, line width)', + 'are scaled, unlike simply setting', + 'a bigger *width* and *height*.' ].join(' ') }, setBackground: { @@ -137,7 +140,6 @@ function toImage(gd, opts) { // extend config for static plot var configImage = Lib.extendFlat({}, config, { staticPlot: true, - plotGlPixelRatio: config.plotGlPixelRatio || 2, setBackground: setBackground }); diff --git a/test/jasmine/tests/snapshot_test.js b/test/jasmine/tests/snapshot_test.js index caf86aca991..c5c16cdbf79 100644 --- a/test/jasmine/tests/snapshot_test.js +++ b/test/jasmine/tests/snapshot_test.js @@ -301,5 +301,25 @@ describe('Plotly.Snapshot', function() { .catch(fail) .then(done); }); + + it('should adapt *viewBox* attribute under *scale* option', function(done) { + Plotly.plot(gd, [{ + y: [1, 2, 1] + }], { + width: 300, + height: 400 + }) + .then(function() { + var str = Plotly.Snapshot.toSVG(gd, 'svg', 2.5); + var dom = parser.parseFromString(str, 'image/svg+xml'); + var el = dom.getElementsByTagName('svg')[0]; + + expect(el.getAttribute('width')).toBe('750', 'width'); + expect(el.getAttribute('height')).toBe('1000', 'height'); + expect(el.getAttribute('viewBox')).toBe('0 0 300 400', 'viewbox'); + }) + .catch(fail) + .then(done); + }); }); }); diff --git a/test/jasmine/tests/toimage_test.js b/test/jasmine/tests/toimage_test.js index df348abf85e..c63e9565de4 100644 --- a/test/jasmine/tests/toimage_test.js +++ b/test/jasmine/tests/toimage_test.js @@ -7,6 +7,8 @@ var fail = require('../assets/fail_test'); var customMatchers = require('../assets/custom_matchers'); var subplotMock = require('@mocks/multiple_subplots.json'); +var FORMATS = ['png', 'jpeg', 'webp', 'svg']; + describe('Plotly.toImage', function() { 'use strict'; @@ -31,6 +33,19 @@ describe('Plotly.toImage', function() { }); } + function assertSize(url, width, height) { + return new Promise(function(resolve, reject) { + var img = new Image(); + img.onload = function() { + expect(img.width).toBe(width, 'image width'); + expect(img.height).toBe(height, 'image height'); + resolve(url); + }; + img.onerror = reject; + img.src = url; + }); + } + it('should be attached to Plotly', function() { expect(Plotly.toImage).toBeDefined(); }); @@ -109,18 +124,22 @@ describe('Plotly.toImage', function() { Plotly.plot(gd, fig.data, fig.layout) .then(function() { return Plotly.toImage(gd, {format: 'png'}); }) + .then(function(url) { return assertSize(url, 700, 450); }) .then(function(url) { expect(url.split('png')[0]).toBe('data:image/'); }) .then(function() { return Plotly.toImage(gd, {format: 'jpeg'}); }) + .then(function(url) { return assertSize(url, 700, 450); }) .then(function(url) { expect(url.split('jpeg')[0]).toBe('data:image/'); }) .then(function() { return Plotly.toImage(gd, {format: 'svg'}); }) + .then(function(url) { return assertSize(url, 700, 450); }) .then(function(url) { expect(url.split('svg')[0]).toBe('data:image/'); }) .then(function() { return Plotly.toImage(gd, {format: 'webp'}); }) + .then(function(url) { return assertSize(url, 700, 450); }) .then(function(url) { expect(url.split('webp')[0]).toBe('data:image/'); }) @@ -156,6 +175,20 @@ describe('Plotly.toImage', function() { .then(done); }); + FORMATS.forEach(function(f) { + it('should respond to *scale* option ( format ' + f + ')', function(done) { + var fig = Lib.extendDeep({}, subplotMock); + + Plotly.plot(gd, fig.data, fig.layout) + .then(function() { return Plotly.toImage(gd, {format: f, scale: 2}); }) + .then(function(url) { return assertSize(url, 1400, 900); }) + .then(function() { return Plotly.toImage(gd, {format: f, scale: 0.5}); }) + .then(function(url) { return assertSize(url, 350, 225); }) + .catch(fail) + .then(done); + }); + }); + it('should accept data/layout/config figure object as input', function(done) { var fig = Lib.extendDeep({}, subplotMock); From 633882b8596fc620c122ff14389003e80808ae8f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Etienne=20T=C3=A9treault-Pinard?= Date: Fri, 1 Sep 2017 12:20:00 -0400 Subject: [PATCH 3/3] fix fallback typos --- src/snapshot/svgtoimg.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/snapshot/svgtoimg.js b/src/snapshot/svgtoimg.js index 8778a7fc776..9f1f4a3de91 100644 --- a/src/snapshot/svgtoimg.js +++ b/src/snapshot/svgtoimg.js @@ -34,8 +34,8 @@ function svgToImg(opts) { var canvas = opts.canvas; var scale = opts.scale || 1; - var w0 = opts.width || 150; - var h0 = opts.height || 300; + var w0 = opts.width || 300; + var h0 = opts.height || 150; var w1 = scale * w0; var h1 = scale * h0;