@@ -49,6 +49,7 @@
Print boxfill 'myboxfill' properties
Remove boxfill 'myboxfill' properties
Test variable counts method
+ Test capturing screenshot
diff --git a/package.json b/package.json
index fb960fe..32ee7b2 100644
--- a/package.json
+++ b/package.json
@@ -51,6 +51,7 @@
},
"dependencies": {
"axios": "0.17.1",
+ "b64-to-blob": "1.2.19",
"css-element-queries": "1.0.1",
"hammerjs": "2.0.8",
"monologue.js": "0.3.5",
diff --git a/src/index.js b/src/index.js
index 05fef31..79d1878 100644
--- a/src/index.js
+++ b/src/index.js
@@ -91,6 +91,21 @@ function init(el, renderingType) {
return this.backend.resize(this, newWidth, newHeight);
},
+ // When this resolves, the result should contain some expected keys:
+ //
+ // {
+ // success: boolean (did screenshot succeed on server)
+ // msg: possible error message in case of failure
+ // type: same as "saveType" argument
+ // blob: A Blob containing the binary image data
+ // }
+ //
+ screenshot(saveType = 'png', saveLocal = true, saveRemote = false,
+ remotePath = null, width = null, height = null) {
+ return this.backend.screenshot(this, saveType, saveLocal, saveRemote,
+ remotePath, width, height);
+ },
+
close() {
Object.keys(this.connection).map((k) => {
return this.connection[k].then((c) => {
diff --git a/src/vtkweb/index.js b/src/vtkweb/index.js
index 02a1fea..c91ba0c 100644
--- a/src/vtkweb/index.js
+++ b/src/vtkweb/index.js
@@ -1,9 +1,17 @@
+import b64toBlob from 'b64-to-blob';
import RemoteRenderer from 'ParaViewWeb/NativeUI/Canvas/RemoteRenderer';
import SizeHelper from 'ParaViewWeb/Common/Misc/SizeHelper';
import SmartConnect from 'wslink/src/SmartConnect';
import { createClient } from 'ParaViewWeb/IO/WebSocket/ParaViewWebClient';
+const mimeTypeMap = {
+ ps: 'application/postscript',
+ pdf: 'application/pdf',
+ png: 'image/png',
+ svg: 'image/svg+xml',
+};
+
const backend = {
// RemoteRenderer associated with a windowId
_renderer: {},
@@ -124,6 +132,33 @@ const backend = {
return rendererPromise;
});
},
+ screenshot(canvas, saveType, saveLocal, saveRemote, remotePath, width, height) {
+ if (!canvas.windowId) {
+ return Promise.resolve(false);
+ }
+ return canvas.connection.vtkweb.then((client) => {
+ const args = [
+ canvas.windowId,
+ saveType,
+ saveLocal,
+ saveRemote,
+ remotePath,
+ width,
+ height,
+ ];
+ return client.pvw.session.call('vcs.canvas.screenshot', args).then((result) => {
+ if (result && result.encodedImage) {
+ const blob = b64toBlob(result.encodedImage, mimeTypeMap[saveType]);
+ const newResult = Object.assign({}, result);
+ delete newResult.encodedImage;
+ newResult.blob = blob;
+ newResult.type = saveType;
+ return newResult;
+ }
+ return result;
+ });
+ });
+ },
clear(canvas) {
if (!canvas.windowId) {
return Promise.resolve(false);
diff --git a/test/cases/vtkweb.js b/test/cases/vtkweb.js
index 0046914..3143631 100644
--- a/test/cases/vtkweb.js
+++ b/test/cases/vtkweb.js
@@ -20,7 +20,7 @@ function getOrCreateColorMap(vcs, name) {
describe('endToEnd', function endToEnd() {
- this.timeout(5000);
+ this.timeout(10000);
it('rendersABoxfillImage', function () {
const testName = this.test.title;
@@ -151,4 +151,38 @@ describe('endToEnd', function endToEnd() {
});
});
});
+
+ it('capturesABase64Png', function () {
+ const testName = this.test.title;
+ const { vcs, container } = getTestRequirements(testName);
+
+ const canvas = vcs.init(container);
+ const vars = {
+ u: {
+ uri: 'clt.nc',
+ variable: 'u',
+ },
+ v: {
+ uri: 'clt.nc',
+ variable: 'v',
+ },
+ };
+
+ return canvas.plot([vars.u, vars.v], ['vector', 'default']).then(() => {
+ return canvas.screenshot('png', true, false, null).then((result) => {
+ return new Promise((resolve, reject) => {
+ if (result.success && result.blob && result.blob.type === 'image/png') {
+ resolve(true);
+ }
+ const msga = `Expected result.success === true (actually ${result.success})`;
+ const msgb = `, and result.blob to be defined (actually ${result.blob})`;
+ let msgc = '';
+ if (result.blob) {
+ msgc = `, and result.blob.type === \'image/png\' (actually \'${result.blob.type}\'')`
+ }
+ reject(new Error(`${msga}${msgb}${msgc}`));
+ });
+ });
+ });
+ });
});
diff --git a/vcs_server/Visualizer.py b/vcs_server/Visualizer.py
index 4a250d8..cbae0b8 100644
--- a/vcs_server/Visualizer.py
+++ b/vcs_server/Visualizer.py
@@ -9,6 +9,9 @@
import cdms2
import genutil
import cdutil
+import sys
+import base64, os, shutil, tempfile
+import traceback
import numpy
import compute_graph
import cdat_compute_graph
@@ -17,6 +20,7 @@
class Visualizer(protocols.vtkWebProtocol):
+ _allowedScreenshotTypes = [ 'png', 'ps', 'pdf', 'svg' ]
_canvas = {}
@exportRpc('vcs.canvas.plot')
@@ -101,6 +105,94 @@ def close(self, windowId):
return True
return False
+ @exportRpc('vcs.canvas.screenshot')
+ def screenshot(self, windowId, saveType, local, remote,
+ remotePath, width, height):
+ """ Save a screenshot of the current canvas
+
+ This method can be used to save a screenshot of the current canvas, as
+ long as the canvas has been plotted once before.
+
+ Arguments:
+ windowId -- Returned by 'plot' method, used to find associated canvas
+ saveType -- One of 'png', 'svg', 'pdf', or 'ps'
+ local -- Should screenshot be saved locally on the client
+ remote -- Should screenshot be save remotely on the server
+ remotePath -- Full path and filename to use when saving remotely
+ width -- Integer giving width of saved screenshot (if different than last plot)
+ height -- Integer giving height of saved screenshot (if different than last plot)
+
+ Returns:
+ A dictionary is returned, including indication of success or failure, as
+ well as any error message, e.g.
+
+ {
+ 'success': True,
+ 'msg': 'Screenshot successfully saved on server' ,
+ 'encodedImage':
+ }
+ """
+ if not windowId in self._canvas:
+ return {
+ 'success': False,
+ 'msg': 'windowId is required to find associated canvas'
+ }
+
+ if not saveType in self._allowedScreenshotTypes:
+ return {
+ 'success': False,
+ 'msg': '%s not in allowed screenshot types' % saveType
+ }
+
+ if remote and remotePath is None:
+ return {
+ 'success': False,
+ 'msg': 'Must have remotePath to save screenshot on server, none provided'
+ }
+
+ # If not saving remotely, we will need a temporary dir/file for
+ # saving the screenshot
+ if not remote:
+ temporaryDirectoryName = tempfile.mkdtemp()
+ remotePath = os.path.join(temporaryDirectoryName, 'tempscreenshot.%s' % saveType)
+ print('Using temporary location for saving image: %s' % remotePath)
+
+ canvas = self._canvas[windowId];
+
+ try:
+ if saveType == 'png':
+ canvas.png(remotePath, width=width, height=height)
+ elif saveType == 'svg':
+ canvas.svg(remotePath, width=width, height=height)
+ elif saveType == 'pdf':
+ canvas.pdf(remotePath, width=width, height=height)
+ elif saveType == 'ps':
+ canvas.postscript(remotePath, width=width, height=height)
+ except Exception as inst:
+ print('Caught exception saving screenshot on server:')
+ print(inst)
+ return {
+ 'success': False,
+ 'msg': 'Failed to save screenshot on server: %s' % remotePath
+ }
+
+ returnValue = {
+ 'success': True,
+ 'msg': 'Screenshot successfully saved on server'
+ }
+
+ if local:
+ with open(remotePath, 'rb') as f:
+ data = f.read()
+ encodedData = base64.b64encode(data)
+ returnValue['encodedImage'] = encodedData
+
+ # If not saving remotely, clean up the temporary directory
+ if not remote:
+ shutil.rmtree(temporaryDirectoryName)
+
+ return returnValue
+
# ======================================================================
# Common elements routines
@exportRpc('vcs.listelements')
|