Skip to content

Commit

Permalink
Merge pull request #64 from CDAT/add-save-screenshot-capability
Browse files Browse the repository at this point in the history
Add save screenshot capability
  • Loading branch information
downiec authored Nov 6, 2018
2 parents 79e8537 + 7e17538 commit e4e75af
Show file tree
Hide file tree
Showing 7 changed files with 240 additions and 2 deletions.
60 changes: 60 additions & 0 deletions demo/demo.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ let canvas;
let renderer;
let boxfill;
let variables;
let pdfBlobUrl;
let pngBlobUrl;


// const { vcs } = window;

Expand Down Expand Up @@ -105,6 +108,63 @@ function vcsBoxfillResize() {
console.log('div resize');
}

function captureCanvasScreenshot() {
canvas.screenshot('pdf', true, false, null).then((result) => {
console.log('Got screenshot result:');
console.log(result);
const { blob, type } = result;
pdfBlobUrl = URL.createObjectURL(blob);
var link = document.createElement("a");
link.href = pdfBlobUrl;
const fname = `image.${type}`;
link.download = fname;
link.innerHTML = `Click here to download ${fname}`;
document.body.appendChild(link);
});

canvas.screenshot('png', true, false, null, 1024, 768).then((result) => {
console.log('Got screenshot result:');
console.log(result);
const { blob, type } = result;
pngBlobUrl = URL.createObjectURL(blob);

var img = document.createElement("img");
img.classList.add("obj");
img.file = blob;
img.width = 200;
img.height = 176;

var link = document.createElement("a");
link.href = pngBlobUrl;
const fname = `image.${type}`;
link.download = fname;
link.appendChild(img);
document.body.appendChild(link);

var reader = new FileReader();
reader.onload = function(e) {
img.src = e.target.result;
};
reader.readAsDataURL(blob);
});
}

function cleanup() {
/*
According the to the documentation here:
https://developer.mozilla.org/en-US/docs/Web/API/URL/createObjectURL
the objectURLs will be revoked when the document is unloaded, so this
isn't technically needed in the case of this demo. However it is
included as a reminder of the proper approach which should be taken
if necessary/possible on a case by case basis.
*/
console.log('Cleaning up object URLs');
URL.revokeObjectURL(pdfBlobUrl);
URL.revokeObjectURL(pngBlobUrl);
}

/**
* Prints the result from get_variables to the console.
* @param {string? filename An absolute path to a netcdf file
Expand Down
3 changes: 2 additions & 1 deletion demo/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
<link rel="icon" href="data:;base64,iVBORw0KGgo=">
</link>
</head>
<body>
<body onbeforeunload="cleanup()">
<table>
<tr>
<td><div id="vcs-boxfill"></div>
Expand Down Expand Up @@ -49,6 +49,7 @@
<li><a onclick="printgraphicsmethod('boxfill', 'myboxfill')" href="javascript:void(0)">Print boxfill 'myboxfill' properties</a></li>
<li><a onclick="removegraphicsmethod('boxfill', 'myboxfill')" href="javascript:void(0)">Remove boxfill 'myboxfill' properties</a></li>
<li><a onclick="printvariablecounts()" href="javascript:void(0)">Test variable counts method</a></li>
<li><a onclick="captureCanvasScreenshot()" href="javascript:void(0)">Test capturing screenshot</a></li>
</ul>

<script type="text/javascript" src="http://localhost:9000/vcs.js"></script>
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
15 changes: 15 additions & 0 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) => {
Expand Down
35 changes: 35 additions & 0 deletions src/vtkweb/index.js
Original file line number Diff line number Diff line change
@@ -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: {},
Expand Down Expand Up @@ -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);
Expand Down
36 changes: 35 additions & 1 deletion test/cases/vtkweb.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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}`));
});
});
});
});
});
92 changes: 92 additions & 0 deletions vcs_server/Visualizer.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -17,6 +20,7 @@

class Visualizer(protocols.vtkWebProtocol):

_allowedScreenshotTypes = [ 'png', 'ps', 'pdf', 'svg' ]
_canvas = {}

@exportRpc('vcs.canvas.plot')
Expand Down Expand Up @@ -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': <base64-encoded-image-data>
}
"""
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')
Expand Down

0 comments on commit e4e75af

Please sign in to comment.