The other day I wanted to quickly put a speech bubble on an image but surprisingly I found no online tool that would enable me to do that apart from some Flash based ones that I didn’t find particularly usable and one HTML5 one which is cool but I didn’t like the bubble style.
So I decided to make one as part of my let’s learn javascript and nodejs journey. There are so many very cool nodejs projects and blog entries out there that I don’t see much point of going into too many technical details of how the project was done, the source code is on GitHub anyway, you can take look if you are interested. But again, I’m only learning both JavaScript and NodeJS so I’m not in any way suggesting that the methods I use are the way to go, but they do the job which I can prove to you if you care to visit the website and put some speech bubbles: http://comicr.co.uk.
There were a few gotchas in the project which I had to do fair amount of googling around to sort out, so in the hope that I can make that process less painful for someone I will summarize here what I’ve learned.
The use case is that after you put the speech bubble on your image you maybe wanna download it to your computer. This is not really nodejs specific task, but this is how it can be done in node:
app.get('/downloadImage', function(req, res) {
var fn = 'speechBubble.png';
res.header('Content-Type', 'image/png');
res.header('Content-Disposition', 'attachment; filename="'+fn+'"');
// ...
// write the png stream to res
// ...
}
On the client side you do a window.location = “/downloadImage” + …; or <a href …> and the browser will either pop-up a dialog where you can change the destination filename (firefox) or just go ahead and download the file using the given name (chrome).
One of the main appeals of NodeJS is that in enables sharing code between the backend (node) and the frontend (browser). In this project I could actually take advantage of this approach as I need to draw the bubbles in the browser as well as in node when rendering the image to png. I used the node-canvas which is an HTML5 canvas implementation for NodeJS using the cairo library. The way I did the code sharing is the following:
- I created a file called drawApi.js which has a drawBubble method.
- On the client I call this method whenever the user places a bubble somewhere, or resizes it, or changes the text.
- On the server I wrap it with drawApi = require(‘drawApi’) then call it when I’m rendering the image.
- The two images are not _exactly_ the same but they are close enough (I would say 99% perceived similarity).
This is not a too sophisticated use of code sharing, but still it’s quite useful. For instance if I want add new bubble types or effects I won’t have to replicate that code just stuff it into drawApi.
Quite a few node libraries are actually written in C++ with a very thin js wrapper around them so you can call the methods from javascript. This is partly cool, cause they are obviously very performant, partly uncool when you try to deploy your application somewhere. The aforementioned node-canvas module uses the cairo library for rendering and libjpeg and giflib for the drawImage method of the canvas which let’s you ‘draw’ an image (the source image has to be decoded). This was not a problem when I was developing on my mac, I just used brew to compile these libraries and then npm took care of linking the node module nicely with them and all was working fine. I was much less happy when I tried to deploy the app to a number of PaaSes that allow NodeJS. To name a couple I tried Nodester, Cloudfoundry. These platforms are probably great for hosting Node apps that don’t hook too deeply in with the host operating system, but I did not succeed to get the node-canvas module to compile on them, as I could not set up the cairo library dependency. Finally I ended up hosting the app on a dedicated centos machine where I could install the libraries manually. This is just something to consider if you plan to host your application on cloud platforms.
The upload image to imgur functionality required me to do an HTTP POST to imgur. I didn’t find any example in the NodeJS documentation to do it so but found an answer on stackoverflow. In case someone doesn’t find it I’ll include the example:
var post_data = querystring.stringify({
'key' : 'imgur_api_key',
'image': canvas.toBuffer().toString("base64") // base64 encode the png stream
});
var post_options = {
host: 'api.imgur.com',
port: '80',
path: '/2/upload.json',
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'Content-Length': post_data.length
}
};
var post_req = http.request(post_options, function(pres) {
pres.setEncoding('utf8');
var response = ""; // this will be the response to the POST
pres.on('data', function (chunk) {
response += chunk;
});
pres.on('end', function() {
console.log('Response: ' + response);
var obj = JSON.parse(response); // imgur answers with json
var result = obj.upload.links;
console.dir(result); // this will contain the imgur links (image, imgur page, delete, etc.)
});
});
// post the data
post_req.write(post_data);
post_req.end();
The problem I had with uploading a file was that the upload process occurs in chunks of Buffers but I needed only one buffer that contains the whole image. I read through the documentation of Buffer but couldn’t find a method that would grow the buffer as new chunks arrive so I did the naive solution and just appended the buffers when the data input stream ended:
app.post('/doUpload', function(req, res, next) {
var buffers = [];
var sumLength = 0;
req.on("data", function(chunk) {
buffers.push(chunk);
sumLength += chunk.length;
});
req.on('end', function(chunk) {
console.log("Total Length: ", sumLength);
var pos = 0;
var bigBuffer = new Buffer(sumLength + 1)
for (var i = 0; i < buffers.length; i++) {
buffers[i].copy(bigBuffer, pos, 0);
pos += buffers[i].length;
}
// ...
// bigBuffer now holds the full image as a Buffer
// ...
});
});
That’s pretty much it the rest is mostly trivial but I’m happy to answer any questions concerning to code you may have.