Search code examples
node.jsamazon-s3imagemagickaws-lambdagm

Amazon Lambda function merge multiple s3 images and put to a destination bucket


I was working on a lambda function which will be triggered via the amazon rest api.

I have created the following

  • a POST end point on REST API console
  • a lambda function linked to the REST API

Now as a first try I created the function such that it will accept the param as

{
 "userProfileImagePath":"facebook users profile image path via GET /{user-id}/picture"
}

The lambda function will get the image using request module and then upload to a bucket.

Since I was using multiple modules I have created everything locally and zipped them including the node_modules and uploaded them to lambda function console by specifying the Handler name.

So far its good and it worked pretty well.

Now to extend this I was trying to send 2 profile images - one is from the user - the other is one of his/her friend - Merge both the images into one and then upload the merged image to the destination file.

I tried different approach for the merging and nothing worked. Found some solutions on here and seems like they really do not like streams

Here is what I did so far and as I mentioned both the images are getting uploaded to a bucket however the merging seems to fail, I ran out of idea to accomplish this, would be really helpful if you can give me some directions on how to do it.

Currently it uses async module to do individual tasks

  • First upload both the images to s3 in parallel
  • then download them using s3.getObject in parallel
  • finally on completion of 2nd task try to merge and push back to s3

Basically there will be a template image and over the template image multiple images will be placed (merged), here in the code below I am considering userProfileImagePath as the main template and trying to merge another image into it.

Please let me know if there is a different way to do this.

Here is what I did so far

/**
* This is a test script which runs as a lambda function on amazon 
* The lamda function is linked with an amazon end point
* The script will accept a image url (facebook/gravator) etc and will upload to a destination s3 bucket and returns the url 
* The param looks like 
{
    "userProfileImagePath":"https://fbcdn-profile-a.......",
    "friendProfileImagePath":"https://fbcdn-profile-a......."
}
*/

var exec = require('child_process').exec,
    async = require('async'),
    request = require('request'),
    gm = require('gm'),
    imageMagick = gm.subClass({ imageMagick: true }),
    aws = require('aws-sdk');

exports.handler = function(req, context) {
    var errMsg = '',
        userProfileImageName = 'user_profile',
        friendProfileImageName = 'friend_profile',
        mergedImageName = 'final_image',
        destinationBucket = 'destination_bucket',
        response = {} ,
        s3 = new aws.S3();

    if (req.userProfileImagePath === '') {
        errMsg = 'Missing the userProfileImage';
    }

    if (req.friendProfileImagePath === '') {
        errMsg = 'Missing the friendProfileImagePath ';
    }

    if (errMsg === '') {
        async.auto({
            copyUserImageToS3 : function(autoCallback) {
                console.log('MyImage :'+req.userProfileImagePath);
                //var myImageData = {} ;
                request({
                    url: req.userProfileImagePath,
                    encoding: null
                }, function(err, res, body) {
                    if (err) { return autoCallback(err); }

                    s3.putObject({
                        Bucket: destinationBucket,
                        Key: userProfileImageName+'.jpg',
                        ContentType: res.headers['content-type'],
                        ContentLength: res.headers['content-length'],
                        Body: body, // buffer
                        ACL:'public-read'
                    }, autoCallback); 
                });
            },

            copyFriendImageToS3 : function(autoCallback) {
                console.log('FriendImage :'+req.friendProfileImagePath);
                //var friendImageData = {} ;
                request({
                    url: req.friendProfileImagePath,
                    encoding: null
                }, function(err, res, body) {
                    if (err) { return autoCallback(err); }  

                    s3.putObject({
                        Bucket: destinationBucket,
                        Key: friendProfileImageName+'.jpg',
                        ContentType: res.headers['content-type'],
                        ContentLength: res.headers['content-length'],
                        Body: body, // buffer
                        ACL:'public-read'
                    }, autoCallback);
               });
            },

            getUserImageFromS3 : ['copyUserImageToS3', function(autoCallback,results) {
                    s3.getObject({
                    Bucket: destinationBucket,
                    Key: userProfileImageName+'.jpg',
                }, autoCallback);
            }],

            getFriendImageFromS3 : ['copyFriendImageToS3', function(autoCallback,results) {
                    s3.getObject({
                    Bucket: destinationBucket,
                    Key: friendProfileImageName+'.jpg',
                }, autoCallback);
            }],

            mergeImages : ['getUserImageFromS3','getFriendImageFromS3', function(autoCallback,results) {
                console.log('Here');
                gm()
                .in('-page', '+0+0')  // Custom place for each of the images
                .in(results.getUserImageFromS3.body)
                .in('-page', '+100+100')
                .in(results.getFriendImageFromS3.body)
                .minify()  
                .mosaic()  
                .toBuffer('JPG', function (err, buffer) {
                        if (err) { return autoCallback(err); }
                        s3.putObject({
                        Bucket: destinationBucket,
                        Key: mergedImageName+'.jpg',
                        ContentType:  results.getUserImageFromS3.headers['content-type'],
                        ContentLength: results.getUserImageFromS3.headers['content-length'],
                        Body: buffer, // buffer
                        ACL:'public-read'
                    }, autoCallback);
                });
            }],
        },
        function(err, results) {
            if (err) {
                response = {
                    "stat":"error",
                    "msg":"Error manipulating the image :: "+err 
                } ;
                context.done(null,response);
            } else {
                response = {
                    "stat":"ok",
                    "imageUrl":"https://s3-................../"+mergedImageName+".jpg"
                } ;
                context.done(null,response);
            }
        });
    } else {
        response = {
            "stat":"error",
            "msg": errMsg
        } ;
        context.done(null,response);
    }
};

UPDATE

I tried to have the code run locally after doing some changes, and use the file system for the operation and it appears to be working, not sure how to make it working the same on lamba using s3 Here is the working code locally using the file system

/**
* This will download both the images locally and then merge them 
* We can merge multiple images as we need on a base template specifiying the position as shown in the code
* need to make sure that the graphicsmagick is inatalled 
* sudo apt-get install graphicsmagick
*/
var exec = require('child_process').exec,
    async = require('async'),
    request = require('request'),
    fs = require('fs'),
    gm = require('gm'),
    imageMagick = gm.subClass({ imageMagick: true }),
    userProfileImagePath ='https://fbcdn-profile-a.akamaihd.net...',
    friendProfileImagePath ='https://fbcdn-profile-a.akamaihd.net..';


exports.mergeFile = function(req, context) {
    var errMsg = '',
        userProfileImageName = 'user_profile',
        friendProfileImageName = 'friend_profile',
        mergedImageName = 'final_image',
        destinationBucket = 'testimagemanipulator',
        response = {} ;

    if (errMsg === '') {
        async.auto({
            copyUserImage : function(autoCallback) {
                request({
                    url: userProfileImagePath,
                    encoding: null
                }, function(err, res, body) {
                    if (err) { return autoCallback(err); }

                    fs.writeFile(__dirname +'/public/images/'+userProfileImageName+'.jpg', body, 'binary', function(err) {
                        if(err) { return autoCallback(err); }

                        return autoCallback();
                    }); 
                });
            },

            copyFriendImage : function(autoCallback) {
                request({
                    url: friendProfileImagePath,
                    encoding: null
                }, function(err, res, body) {
                    if (err) { return autoCallback(err); }  

                    fs.writeFile(__dirname +'/public/images/'+friendProfileImageName+'.jpg', body, 'binary', function(err) {
                        if(err) { return autoCallback(err); }

                        return autoCallback();
                    });
                });
            },

            mergeImages : ['copyUserImage','copyFriendImage', function(autoCallback,results) {
                console.log('Here');
                gm()
                .in('-page', '+0+0')  // Custom place for each of the images
                .in(__dirname +'/public/images/'+userProfileImageName+'.jpg')
                .in('-page', '+140+50')
                .in(__dirname +'/public/images/'+friendProfileImageName+'.jpg')
                .minify()  // Halves the size, 512x512 -> 256x256
                .mosaic()  // Merges the images as a matrix
                .write(__dirname +'/public/images/'+mergedImageName+'.jpg', function (err) {
                        if (err) { return autoCallback(err); }

                        fs.unlink(__dirname +'/public/images/'+userProfileImageName+'.jpg');
                        fs.unlink(__dirname +'/public/images/'+friendProfileImageName+'.jpg');
                        return autoCallback() ;
                });
            }],
        },
        function(err, results) {
            if (err) {
                response = {
                    "stat":"error",
                    "msg":"Error manipulating the image :: "+err 
                } ;
                console.log(response) ;
            } else {
                response = {
                    "stat":"ok",
                    "imageUrl":__dirname +'/public/images/'+mergedImageName+'.jpg'
                } ;
                console.log(response) ;
            }
        });
    } else {
        response = {
            "stat":"error",
            "msg": errMsg
        } ;
        console.log(response) ;
    }
};

module.exports.mergeFile() ;

Solution

  • Ok looks like it would be difficult to achieve what I was looking as explained into the question above. However after doing several try and different modules none of the try worked. As explained the purpose of the lambda function is

    • Get a public image urls (facebook)

    • Download them and merge together, it could be merged over an image template also

    • Write some text into the merged image.

    So far the best image manipulation module is gm and this could be used on aws lambda all we need to make sure its a subclass of Image Magic

    The next is merging operation could be done by using aws /tmp folder where the images could be placed temporarily and after the merging is done they could be removed from there.

    Here is a working lambda script which is linked to an aws REST API

    /**
    * This is a test script which runs as a lambda function on amazon 
    * The lamda function is linked with an amazon end point
    * The script will accept a image url (facebook/gravator) etc and will upload to a destination s3 bucket and returns the url 
    * The param looks like 
    {
        "userProfileImagePath":"https://fbcdn-profile-a.akamaihd.net/....",
        "friendProfileImagePath":"https://fbcdn-profile-a.akamaihd.net/...."
    }
    */
    
    var exec = require('child_process').exec,
        async = require('async'),
        request = require('request'),
        gm = require('gm').subClass({ imageMagick: true }),
        fs = require('fs'),
        aws = require('aws-sdk');
    
    exports.handler = function(req, context) {
        var errMsg = '',
            userProfileImageName = 'user_profile',
            friendProfileImageName = 'friend_profile',
            mergedImageName = 'final_image',
            destinationBucket = 'mybucket',
            response = {} ,
            s3 = new aws.S3();
    
        if (req.userProfileImagePath === '') {
            errMsg = 'Missing the userProfileImage';
        }
    
        if (req.friendProfileImagePath === '') {
            errMsg = 'Missing the friendProfileImagePath ';
        }
    
        if (errMsg === '') {
            async.auto({
                copyUserImage : function(autoCallback) {
                    request({
                        url: req.userProfileImagePath,
                        encoding: null
                    }, function(err, res, body) {
                        if (err) { return autoCallback(err); }
    
                        fs.writeFile('/tmp/'+userProfileImageName+'.jpg', body, 'binary', function(err) {
                            if(err) { return autoCallback(err); }
    
                            return autoCallback();
                        }); 
                    });
                },
    
                copyFriendImage : function(autoCallback) {
                    request({
                        url: req.friendProfileImagePath,
                        encoding: null
                    }, function(err, res, body) {
                        if (err) { return autoCallback(err); }  
    
                        fs.writeFile('/tmp/'+friendProfileImageName+'.jpg', body, 'binary', function(err) {
                            if(err) { return autoCallback(err); }
    
                            return autoCallback();
                        });
                    });
                },
    
                mergeImages : ['copyUserImage','copyFriendImage', function(autoCallback,results) {
                    var bgImage = '/tmp/'+userProfileImageName+'.jpg',
                        frontImage = '/tmp/'+friendProfileImageName+'.jpg';
    
                    gm()
                    .in('-page', '+0+0')  // Custom place for each of the images
                    .in(bgImage)
                    .in('-page', '+140+50')
                    .in(frontImage)
                    .mosaic()  // Merges the images as a matrix
                    .font("Arial")
                    .fontSize(50)
                    .fill('black')
                    .drawText(1, 1, 'Hello World', 'Center')
                    .fill('blue')
                    .drawText(0, 0, 'Hello World', 'Center')
                    .write('/tmp/'+mergedImageName+'.jpg', function (err) {
                            if (err) { return autoCallback(err); }
    
                            var stream = fs.createReadStream('/tmp/'+mergedImageName+'.jpg');
                            var stats = fs.statSync('/tmp/'+mergedImageName+'.jpg');
                            console.log('Merged File size :'+stats['size']);
                            s3.upload({
                                Bucket: destinationBucket,
                                Key: mergedImageName+'.jpg',
                                ContentType: 'image/jpeg',
                                ContentLength: stats['size'],
                                Body: stream, // buffer
                                ACL:'public-read'
                        }, autoCallback);   
                    });
                }],
            },
            function(err, results) {
                if (err) {
                    response = {
                        "stat":"error",
                        "msg":"Error manipulating the image :: "+err 
                    } ;
                    context.done(null,response);
                } else {
                    response = {
                        "stat":"ok",
                        "imageUrl":"https://domain.amazonaws.com/mybucket/"+mergedImageName+".jpg"
                    } ;
                    context.done(null,response);
                }
            });
        } else {
            response = {
                "stat":"error",
                "msg": errMsg
            } ;
            context.done(null,response);
        }
    };