Our website is made possible by displaying online advertisements to our visitors. Please consider supporting us by disabling your ad blocker.

Create An Android Launcher Icon Generator RESTful API With Node.js, And Jimp

TwitterFacebookRedditLinkedInHacker News

When you’re developing an Android mobile application, it is critical that you come up with a nice launcher icon for all possible Android screen densities. If you’re not too familiar with Android, there are mdpi, hdpi, xhdpi, xxhdpi, and xxxhdpi densities as of now. This number could change in the future.

Once you’ve got your icon, resampling or resizing it for each possible screen density can become a pain in the butt. Instead, it makes sense to use or create a script for this.

If you’ve been keeping up, you’ll remember I wrote about image generators in an article titled, Generating Splash Screens and Application Icons for NativeScript Mobile Apps. Since we’re talented developers, we’re going to create our own service this time around.

We’re going to see how to create a RESTful API that accepts an image and generates various sizes of that same image, bundled within a ZIP archive. We’re going to accomplish this task with Node.js and Express.

Create a New Node.js Project

To be successful with this project, it will help to start fresh. With Node.js installed, execute the following from somewhere on your computer:

npm init -y
npm install express body-parser multer bluebird jimp node-zip --save
touch app.js

The above commands will initialize a new project with a package.json file, install our dependencies, and create an app.js file to hold our logic. If you don’t have the touch command, create the file however you see fit.

Before we start development, let’s have another look at those dependencies. We’ll be serving an API, hence the express package. To receive POST bodies, we’ll need the body-parser package. To receive files, we’ll need the multer package. More information on receiving files in an Express based project can be found in an article I wrote titled, Upload Files to a Minio Object Storage Cloud with Node.js and Multer.

The jimp package will allow us to do image manipulations with JavaScript and the node-zip package will allow us to create ZIP archives. Since everything we do is asynchronous, we’ll need the help of the bluebird package to turn things into promises and monitor them.

Including the Application Logic for Jimp, Bluebird, and File Archiving

With the project created, let’s dive into some code. It makes sense to add our boilerplate code for bootstrapping our API before we worry ourselves with the heavy lifting of image manipulations.

Open the project’s app.js file and include the following:

var Zip = new require('node-zip')();
var Bluebird = require("bluebird");
var Jimp = require("jimp");
var Express = require("express");
var Multer = require("multer");
var BodyParser = require("body-parser");
Jimp.prototype.getBufferAsync = Bluebird.promisify(Jimp.prototype.getBuffer);

var app = Express();

app.use(BodyParser.json({limit: "4mb"}));

app.post("/process", Multer({storage: Multer.memoryStorage()}).single("upload"), (request, response) => { });

app.listen(3000, () => {
    console.log("Listening...");
});

In the above code we’ve imported each of our downloaded dependencies. Not every method in Jimp uses promises, but instead callbacks. It is very important for this example that we use promises which is why we’re wrapping Jimp with Bluebird.

We’re specifying that we’re accepting files up to 4MB in size and the FormData key must be uploads. All uploads will be held in memory, not saved to the filesystem.

When the API endpoint receives data, we need to load it with Jimp:

app.post("/process", Multer({storage: Multer.memoryStorage()}).single("upload"), (request, response) => {
    Jimp.read(request.file.buffer, (error, image) => {
        if(error) {
            throw error;
        }
    });
});

The image is read from the file buffer and if there are no errors, we can do our manipulations. This is where things can get a little confusing.

Android expects multiple images. We have no intention of saving these images to the Node.js server. For this reason we’ll have to keep track of each asynchronous operation.

app.post("/process", Multer({storage: Multer.memoryStorage()}).single("upload"), (request, response) => {
    Jimp.read(request.file.buffer, (error, image) => {
        if(error) {
            throw error;
        }
        var images = [];
        images.push(image.resize(196, 196).getBufferAsync(Jimp.MIME_PNG).then(result => {
            return new Bluebird((resolve, reject) => {
                resolve({
                    size: "xxxhdpi",
                    data: result
                });
            });
        }));
        images.push(image.resize(144, 144).getBufferAsync(Jimp.MIME_PNG).then(result => {
            return new Bluebird((resolve, reject) => {
                resolve({
                    size: "xxhdpi",
                    data: result
                });
            });
        }));
        images.push(image.resize(96, 96).getBufferAsync(Jimp.MIME_PNG).then(result => {
            return new Bluebird((resolve, reject) => {
                resolve({
                    size: "xhdpi",
                    data: result
                });
            });
        }));
        images.push(image.resize(72, 72).getBufferAsync(Jimp.MIME_PNG).then(result => {
            return new Bluebird((resolve, reject) => {
                resolve({
                    size: "hdpi",
                    data: result
                });
            });
        }));
        images.push(image.resize(48, 48).getBufferAsync(Jimp.MIME_PNG).then(result => {
            return new Bluebird((resolve, reject) => {
                resolve({
                    size: "mdpi",
                    data: result
                });
            });
        }));
    });
});

Notice that we’re populating an array with each image related operation. Each operation sizes the image differently and outputs a buffer. The buffer alone isn’t useful to use which is why we have a chained promise that includes the buffer as well as the image size that the buffer represents.

With the array of executing promises, we can monitor them with the following:

Bluebird.all(images).then((data) => {
    for(var i = 0; i < data.length; i++) {
        Zip.file(data[i].size + "/icon.png", data[i].data);
    }
    var d = Zip.generate({ base64: false, compression: "DEFLATE" });
    response.set({ "Content-disposition": "attachment;filename=android.zip" });
    response.send(new Buffer(d, "binary"));
});

The Bluebird.all method will take our array and resolve once every promise in the array has resolved or rejected. The data will be the buffers and their matching image sizes.

We can loop through each image data and add it to the ZIP archive. The image size will represent the directory for the icon.

Remember, this is an API endpoint, so we need to send a response when we’re done. This response will have a header to specify the download name as well as send the buffer back to the client.

The full JavaScript code would look like the following:

var Zip = new require('node-zip')();
var Bluebird = require("bluebird");
var Jimp = require("jimp");
var Express = require("express");
var Multer = require("multer");
var BodyParser = require("body-parser");
Jimp.prototype.getBufferAsync = Bluebird.promisify(Jimp.prototype.getBuffer);

var app = Express();

app.use(BodyParser.json({limit: "4mb"}));

app.post("/process", Multer({storage: Multer.memoryStorage()}).single("upload"), (request, response) => {
    Jimp.read(request.file.buffer, (error, image) => {
        if(error) {
            throw error;
        }
        var images = [];
        images.push(image.resize(196, 196).getBufferAsync(Jimp.MIME_PNG).then(result => {
            return new Bluebird((resolve, reject) => {
                resolve({
                    size: "xxxhdpi",
                    data: result
                });
            });
        }));
        images.push(image.resize(144, 144).getBufferAsync(Jimp.MIME_PNG).then(result => {
            return new Bluebird((resolve, reject) => {
                resolve({
                    size: "xxhdpi",
                    data: result
                });
            });
        }));
        images.push(image.resize(96, 96).getBufferAsync(Jimp.MIME_PNG).then(result => {
            return new Bluebird((resolve, reject) => {
                resolve({
                    size: "xhdpi",
                    data: result
                });
            });
        }));
        images.push(image.resize(72, 72).getBufferAsync(Jimp.MIME_PNG).then(result => {
            return new Bluebird((resolve, reject) => {
                resolve({
                    size: "hdpi",
                    data: result
                });
            });
        }));
        images.push(image.resize(48, 48).getBufferAsync(Jimp.MIME_PNG).then(result => {
            return new Bluebird((resolve, reject) => {
                resolve({
                    size: "mdpi",
                    data: result
                });
            });
        }));
        Bluebird.all(images).then((data) => {
            for(var i = 0; i < data.length; i++) {
                Zip.file(data[i].size + "/icon.png", data[i].data);
            }
            var d = Zip.generate({ base64: false, compression: "DEFLATE" });
            response.set({ "Content-disposition": "attachment;filename=android.zip" });
            response.send(new Buffer(d, "binary"));
        });
    });
});

app.listen(3000, () => {
    console.log("Listening...");
})

The code is quite long, but honestly it is very repetitive. Remember, we’re only accepting an image from the client, reading the image file and resizing it five times, then creating an archive after all image manipulations have completed.

Testing the RESTful API with cURL

With the single endpoint API created, we can make an attempt at processing our image for Android compatibility. To be successful in testing, we probably want to use cURL rather than a utility like Postman because we intend to download data rather than display it.

Before we try to test the code, let’s first run it. From the command line, execute the following:

node app.js

The above command will start serving our application at http://localhost:3000/process. Remember, the /process endpoint only accepts POST requests.

Assuming that cURL is available on your computer, execute the following:

curl -X POST -F upload=@./myimage.jpg http://localhost:3000/process >> android.zip

The above command will issue a multi-part form HTTP request to our endpoint. By using >> we’re saying we want to save the response to a file. The upload=@./myimage.jpg is the local file we wish to send, hence the @ symbol.

Try to extract the ZIP archive that was downloaded. The content should include numerous directories with files of varying sizes.

Conclusion

You just saw how to create a RESTful API that manipulates images and returns them to the client in the form of a ZIP archive. This particular example created Android launcher icons from some main icon, but it could be expanded into many other things. Node.js with Express and Jimp are very powerful when it comes to web applications and image processing.

Want to upload the files with Angular rather than cURL. Check out a previous article I wrote on the topic.

Nic Raboy

Nic Raboy

Nic Raboy is an advocate of modern web and mobile development technologies. He has experience in C#, JavaScript, Golang and a variety of frameworks such as Angular, NativeScript, and Unity. Nic writes about his development experiences related to making web and mobile development easier to understand.