Monday 15 August 2016

AWS Lambda - more than ImageMagick and configuring API Gateway

We've looked at getting setup a basic Lambda function that returns a scaled version of a file held on S3. Not very exciting or dynamic. It's great that ImageMagick is available as a package for Lambda and this means that some pretty basic Lambdas can be setup just using the inline code page on AWS to get things kicked off. For anything more ImageMagick doesn't quite give us enough. This also pushes us on to the next step of using Lambdas for a bit more functionality.

We're going to take another baby step and look at providing a slightly improved function to take a cutout tile from the image used prevsiously. Imagine we have an image that is composed of a tile of 5 rows and 10 columns of images and give some consideration to a little border region between each. What we want to do is return one of those tiles in the function call.

ImageMagick no longer has enough functionality to help, so we're going to use GraphicsMagick to provide the additional functionality. The code example is shown below:


var im = require("imagemagick");
var fs = require("fs");
var gm = require('gm').subClass({
    imageMagick: true
});
var os = require('os');

var AWS = require('aws-sdk');
AWS.config.region = 'eu-west-1';

exports.handler = function(event, context) {
    
    var params = {Bucket: 'bucket', Key: 'image.png'};
    var s3 = new AWS.S3();
                
    console.log(params);


    s3.getObject(params, function(err, data) {
        if (err) 
            console.log("ERROR", err, err.stack); // an error occurred
        else     
            console.log("SUCCESS", data);         // successful response
        
            var resizedFile = os.tmpdir() + '/' + Math.round(Date.now() * Math.random() * 10000);
        
            console.log("filename", resizedFile); 
            
            
            gm(data.Body).size(function(err, size) {
                
                console.log("Size", size); 
                
                var num = 10;

                var x = size.width / num;
                var y = size.height / 5;
            
                var col = 0;
                var row = 0;
            
                
                gm(data.Body)
                    .crop(x-14, y-14,
                            (col * x)+7,(row * y)+7)
                    .write(resizedFile, function(err) {
                    
                        console.log("resized", resizedFile); 
                        
                        if (err) {
                            console.log("Error", err); 
                            context.fail(err);
                        }
                        else {
                            var resultImgBase64 = new Buffer(fs.readFileSync(resizedFile)).toString('base64');
                            context.succeed({ "img" : resultImgBase64 });
                        }
                        
                    });
                });
                
        });
    
};

In this example we're still not sending in any parameters and we're looking to get back the (0,0) referenced tile in the top-left corner. Using the below image, this would be the blue circle:


If you put this Lambda code directly into the inline editor and try to call it you'll get an error as it cannot find gm, GraphicsMagick. To add it in unfortunately we need to start packaging up our Lambda. So, first step, you need to download the gm package, which is easily done with npm as follows:


npm install gm

If you do this in the root of your project you'll get a folder called nod_modules containing a bunch of files. Now, create a zip file along with your Node,js function above. The contents inside should look like this:





Now in your Lambda you will need to select your Zip file, upload it (by clicking Save - yep, a bit unintuitive) and you'll need to configure the Configuration tab of your Lambda. The Handler will need to be set to call the Node.js filename and the function that you export, so in this case the filename is called 'getImage' and the handler exported has been called 'handler'.







Great. Now things should run smoothly and you'll get back a tile (hopefully the round circle) from the image. To make this a bit more useful it would be good to now pass in the row + column information to be able to choose the tile, so let's do that.

Adjusting the Lambda code we can get parameter information in via the 'event' information passed into the handler function. Let's add a line to log this to the console.


exports.handler = function(event, context) {
    
    console.log('Received event:', JSON.stringify(event, null, 2));

Now configure your test event from the Lambda console - Actions/Configure Test Event to something like this:














And run a test execution, you should get something like this in the log (Click on the Monitoring tab in Lambda and scroll down):












Great, we're calling this from within AWS as a test and getting back the row & column information. It's now simple enough to get these values to set the row & column variables. For the sake of simplicity I'll leave out error handling in case events are not present and if the values are not correct, that's standard code work.


     var col = event.col;
     var row = event.row;

Run again and take a look at the base64 string in the JSON return result 'img'.

So this is getting the parameter information from within AWS but we want to specify it from our simple HTML page, so we need to configure API Gateway to pass the parameters in the HTTP GET method in the traditional kind of 'getImage?row=1&col=1' kind of formulation.

Navigate to the Lambda Triggers tab and click on the 'GET' method which will lead you to the API Gateway configuration.  Choose your API, click on Resources and choose your function to give you this view:














Now click on te Integration Request and scroll down and open out the Body Mapping Templates:


Add in a new Content-Type and paste in the template above which maps the input parameters to the event object.

This should now be ready to go, so a simple test in a browser first and then tweak your HTML code to have something like this:


    var response = $http.get('https://xxxxxx.execute-api.eu-west-1.amazonaws.com/prod/getImage2?row=2&col=2');

It's simple exercise now to play around with your JavaScript in the HTML to select the tile you want to get.

2 comments: