Lets say that you embrace the WebGL world and write some nifty graphics application in it. Now lets suppose you want to save an image or two from your interactive application and doing a screengrab is not appropriate. Perhaps this is because you wish to save many images, such as an animation. Maybe you are debugging a complicated problem and wish to save snapshots in various parts of a process.
Maybe you are iterating through a space and need to make a record of each step. I do this all the time in animation production to do what is called a "wedge", which is a way of making choices among many parameters.
But this is WEBGL and the bold new Internet paradigm, so things are not so easy.
In fact, its a really annoying process to save an image under program control and the reason for this has less to do with WebGL than it does to do with Internet architecture and the awful state of documentation in our bold new Internet based reality.
I present here one solution to the "save an image" problem. Here are choices that had to be made to get to this solution:
1. In order to save an image, you must upload it to a server.
2. In order to upload anything you must use "Ajax", which really means XMHHttpRequest.
3. The server side is written in node.js
4. We are going to use POST.
5. We are going to use base64.
6. We are not going to use FORMS or BLOBS.
7. Our server is going to do nothing but receive an image and save it out.
8. You may save as many images as you like, the filename will be incremented by 1 each time.
9. All images are saved in PNG format.
10. We are going to use getDataURL() directly from the canvas and not use gl.readPixels at all.
11. Therefore we do not have to set any special flags when we acquire the 3D canvas.
12. If a file already exists of the same name, it will be written over without comment.
13. The images are put in the directory that you run the node server out of.
If you are new to this, these choices made above eliminate a billion or so other possible solutions and therefore makes the problem solvable (instead of iterating through an infinite search space getting half baked quasi solutions on the internet).
When it is all said and done, the result is about 1/2 page of code on the browser/client side and about 1 page or a little more on the server side. Everything is written in Javascript, with the server side making use of node.js.
Here is the code that works for me. If you try it and it does not work for you, please let me know.
Uploading An Image (Client/Browser Side)
When the 3D canvas has an image you wish to save, do the following:
// generate "dataURL" for the image on the screen
// "canvas" is what is returned from document.getElementById on your webgl canvas
var newimg_dataurl = canvas.toDataURL("image/png");
// NB The server is going to be looking on socket 8080 and a path of /imagehandler.
// But these are arbitrary and can be whatever you like
s_postimage(newimg_dataurl, "image/png", "http://localhost:8080/imagehandler");
// the function that actually posts the image
var s_postimage = function(dataurl, imagetype, dest_url) {
var xr = XMLHttpRequest();
xr.addEventListener("error", xfer_error, false);
xr.addEventListener("load", xfer_complete, false);
xr.open("POST", dest_url);
xr.send(dataurl);
return;
};
var xfer_complete = function(e) {
// document.write("<br>xfer complete");
};
var xfer_error = function(e) {
if (e) throw e;
};
Uploading an Image (Server Side)
This is run with the command "node imageup.js" where the file imageup.js contains:
var http = require('http');
var url = require('url');
var querystring = require('querystring');
var util = require('util');
var fs = require('fs');
var img_seqno = 0;
http.createServer(function (req, res) {
switch(req.url) {
// This is where you would put in handlers for situations/requests other than
// the request to upload an image
case '/imagehandler':
if (req.method == 'POST') {
console.log("[200] " + req.method + " to " + req.url);
var fullBody = "'';
var fname = "upimage_" + img_seqno.toString() + ".png";
console.log(fname);
req.on('data', function(chunk) {
// append the current chunk of data to the fullBody variable
fullBody += chunk;
console.log("data received");
});
req.on('end', function() {
// request ended -> do something with the data
res.writeHead(200, "OK", {'Content-Type': 'text/html'});
console.log("end received");
var base64Data = fullBody.replace(/^data:image\/png;base64,/,"");
fs.writeFile(fname, base64Data, 'base64', function(err) {
if (err) throw err;
console.log("image saved " + fname);
img_seqno ++;
});
res.end();
});
} else if (req.method == 'OPTIONS') {
var headers = {};
headers["Access-Control-Allow-Origin"] = "*";
headers["Access-Control-Allow-Methods"] = "PUT, POST, GET, DELETE, OPTIONS";
headers["Access-Control-Allow-Credentials"] = false;
headers["Access-Control-Max-Age"] = '86400'; // 24 hours
headers["Access-Control-Allow-Headers"] =
"X-Requested-With, X-HTTP-Method-Override, Content-Type, Accept";
res.writeHead(200, headers);
res.end();
console.log("OPTIONS received");
} else {
console.log("[405] " + req.method + " to " + req.url);
res.writeHead(405, "Method not supported", {'Content-Type': 'text/html'});
res.end('<html><body>Method not supported</body></html>');
}
break;
default:
res.writeHead(404, "Not found", {'Content-Type': 'text/html'});
res.end('<html><body>Not found</body></html>');
console.log("[404] " + req.method + " to " + req.url);
};
}).listen(8080, "127.0.0.1"); // listen on tcp port 8080 on the localhost
// end of server code