Nodejs image resizer with graphicmagick - node.js

I have the following nodejs code, which as is it is, gets an image from AWS, resizes it into 4 different sizes and then saves it back into the AWS bucket into separate folders. However I need to write it so that it can be run on the dev environment as well. How could I write this so that depending on the input (local file on a vagrant machine, or on the AWS server) different functions are called (what to listen to?). It is worth noting that I am using AWS's new service Lambda.
// dependencies
var async = require('async');
var AWS = require('aws-sdk');
var gm = require('gm').subClass({ imageMagick: true });
var util = require('util');
// get reference to S3 client
var s3 = new AWS.S3();
exports.handler = function(event, context) {
// Read options from the event.
console.log("Reading options from event:\n", util.inspect(event, {depth: 5}));
var srcBucket = event.Records[0].s3.bucket.name;
var srcKey = event.Records[0].s3.object.key;
var _800px = {
width: 800,
dstnKey: srcKey,
destinationPath: "large"
};
var _500px = {
width: 500,
dstnKey: srcKey,
destinationPath: "medium"
};
var _200px = {
width: 200,
dstnKey: srcKey,
destinationPath: "small"
};
var _45px = {
width: 45,
dstnKey: srcKey,
destinationPath: "thumbnail"
};
var _sizesArray = [_800px, _500px, _200px, _45px];
var len = _sizesArray.length;
console.log(len);
console.log(srcBucket);
console.log(srcKey);
// Infer the image type.
var typeMatch = srcKey.match(/\.([^.]*)$/);
if (!typeMatch) {
console.error('unable to infer image type for key ' + srcKey);
return;
}
var imageType = typeMatch[1];
if (imageType != "jpg" && imageType != "png") {
console.log('skipping non-image ' + srcKey);
return;
}
// Download the image from S3, transform, and upload to same S3 bucket but different folders.
async.waterfall([
function download(next) {
// Download the image from S3 into a buffer.
s3.getObject({
Bucket: srcBucket,
Key: srcKey
},
next);
},
function transform(response, next) {
for (var i = 0; i<len; i++) {
// Transform the image buffer in memory.
gm(response.Body, srcKey)
.resize(_sizesArray[i].width)
.toBuffer(imageType, function(err, buffer) {
if (err) {
next(err);
} else {
next(null, response.ContentType, buffer);
}
});
}
},
function upload(contentType, data, next) {
for (var i = 0; i<len; i++) {
// Stream the transformed image to a different folder.
s3.putObject({
Bucket: srcBucket,
Key: "dst/" + _sizesArray[i].destinationPath + "/" + _sizesArray[i].dstnKey,
Body: data,
ContentType: contentType
},
next);
}
}
], function (err) {
if (err) {
console.error(
'---->Unable to resize ' + srcBucket + '/' + srcKey +
' and upload to ' + srcBucket + '/dst' +
' due to an error: ' + err
);
} else {
console.log(
'---->Successfully resized ' + srcBucket +
' and uploaded to' + srcBucket + "/dst"
);
}
context.done();
}
);
};

I would go for creating two providers(modules) i.e fsProvider and awsProvider with download, transform and upload methods. Then in handler i will decide which provider to use depending on process.end.NODE_ENV (development or production).

Related

AWS S3 Lambda thumbnail generation node error in cloud watch

My lambda function is not generating thumbnail as expected. Here's is my node code. I uploaded a .zip file with node modules.
// dependencies
var async = require('async');
var AWS = require('aws-sdk');
var gm = require('gm')
.subClass({ imageMagick: true }); // Enable ImageMagick integration.
var util = require('util');
// constants
var MAX_WIDTH = 250;
var MAX_HEIGHT = 250;
// get reference to S3 client
var s3 = new AWS.S3();
exports.handler = function(event, context, callback) {
// Read options from the event.
console.log("Reading options from event:\n", util.inspect(event, {depth: 5}));
var srcBucket = event.Records[0].s3.bucket.name;
// Object key may have spaces or unicode non-ASCII characters.
var srcKey =
decodeURIComponent(event.Records[0].s3.object.key.replace(/\+/g, " "));
var dstBucket = srcBucket + "resized";
var dstKey = "thumb-" + srcKey;
// Sanity check: validate that source and destination are different buckets.
if (srcBucket == dstBucket) {
callback("Source and destination buckets are the same.");
return;
}
// Infer the image type.
var typeMatch = srcKey.match(/\.([^.]*)$/);
if (!typeMatch) {
callback("Could not determine the image type.");
return;
}
var imageType = typeMatch[1].toLowerCase();
if (imageType != "jpg" && imageType != "png") {
callback(`Unsupported image type: ${imageType}`);
return;
}
// Download the image from S3, transform, and upload to a different S3 bucket.
async.waterfall([
function download(next) {
// Download the image from S3 into a buffer.
s3.getObject({
Bucket: srcBucket,
Key: srcKey
},
next);
},
function transform(response, next) {
gm(response.Body).size(function(err, size) {
console.log('Width logggg' + size.width);
console.log('height logggg' + size.height)
// Infer the scaling factor to avoid stretching the image unnaturally.
var scalingFactor = Math.min(
MAX_WIDTH / size.width,
MAX_HEIGHT / size.height
);
var width = scalingFactor * size.width;
var height = scalingFactor * size.height;
// Transform the image buffer in memory.
this.resize(width, height)
.toBuffer(imageType, function(err, buffer) {
if (err) {
next(err);
} else {
next(null, response.ContentType, buffer);
}
});
});
},
function upload(contentType, data, next) {
// Stream the transformed image to a different S3 bucket.
s3.putObject({
Bucket: dstBucket,
Key: dstKey,
Body: data,
ContentType: contentType
},
next);
}
], function (err) {
if (err) {
console.error(
'Unable to resize ' + srcBucket + '/' + srcKey +
' and upload to ' + dstBucket + '/' + dstKey +
' due to an error: ' + err
);
} else {
console.log(
'Successfully resized ' + srcBucket + '/' + srcKey +
' and uploaded to ' + dstBucket + '/' + dstKey
);
}
callback(null, "message");
}
);
};
I've just pasted the code from this link
I implemented everything correct in Lambda, S3 trigger, and bucket names. But in Cloudwatch metrics, I'm getting this error.
Please tell me about my mistake.
How to fix this error.
The stack trace shows that the error is happening in the file /var/task/index.js on line 57, column 38 so that's the best place to start.
If I had to guess though, if we're assuming the transform function is what is in index.js, it might be that size does not have a width property. Might be worth logging there to see what's going on.
function transform(response, next) {
gm(response.Body).size(function(err, size) {
// Infer the scaling factor to avoid stretching the image unnaturally.
var scalingFactor = Math.min(
MAX_WIDTH / size.width, // <-- here
MAX_HEIGHT / size.height
);
Idk what gm(response.Body).size() expects for its callback but it would be good to log the params that get passed here.

s3 Lambda convert command

I am trying to do image optimisation through convert command. if i execute the convert command from command line
convert $1 -sampling-factor 4:2:0 -strip -quality 85 -interlace JPEG -colorspace RGB $2
the image converts good.
But if I execute the Lambda function, image quality is poor. Is there any other way to get this solved. my NodeJS function is
var async = require('async');
var AWS = require('aws-sdk');
var util = require('util');
var im = require('imagemagick');
var fs = require('fs');
// constants
var MAX_WIDTH = 100;
var MAX_HEIGHT = 100;
var s3 = require('s3');
var client = s3.createClient({
maxAsyncS3: 20, // this is the default
s3RetryCount: 3, // this is the default
s3RetryDelay: 1000, // this is the default
multipartUploadThreshold: 20971520, // this is the default (20 MB)
multipartUploadSize: 15728640, // this is the default (15 MB)
s3Options: {
accessKeyId: "xx",
secretAccessKey: "xx",
},
});
exports.handler = function(event, context, callback) {
// Read options from the event.
console.log("Reading options from event:\n", util.inspect(event, {depth: 5}));
var srcBucket = event.Records[0].s3.bucket.name;
// Object key may have spaces or unicode non-ASCII characters.
var srcKey = decodeURIComponent(event.Records[0].s3.object.key.replace(/\+/g, " "));
var dstBucket = srcBucket + "resized";
var dstKey = "resized-" + srcKey;
// Sanity check: validate that source and destination are different buckets.
if (srcBucket == dstBucket) {
callback("Source and destination buckets are the same.");
return;
}
// Infer the image type.
var typeMatch = srcKey.match(/\.([^.]*)$/);
if (!typeMatch) {
callback("Could not determine the image type.");
return;
}
var imageType = typeMatch[1];
if (imageType != "jpg" && imageType != "png") {
callback('Unsupported image type: ${imageType}');
return;
}
// Download the image from S3, transform, and upload to a different S3 bucket.
async.waterfall([
function download(next) {
var params = {
localFile: "/tmp/"+srcKey,
s3Params: {
Bucket: srcBucket,
Key: srcKey,
},
};
var downloader = client.downloadFile(params);
downloader.on('error', function(err) {
console.error("unable to download:", err.stack);
});
downloader.on('progress', function() {
console.log("progress", downloader.progressAmount, downloader.progressTotal);
});
downloader.on('end', function() {
console.log("done downloading");
im.convert(["/tmp/"+srcKey,
'-sampling-factor','4:2:0',
'-strip',
'-quality', '85',
'-interlace', 'JPEG',
'-colorspace','RGB',
"/tmp/"+srcKey],
function(err, stdout){
if (err) throw err;
console.log('stdout:', stdout);
//upload a file
var uploadparams = {
localFile: "/tmp/"+srcKey,
s3Params: {
Bucket: dstBucket,
Key: dstKey,
},
};
var uploader = client.uploadFile(uploadparams);
uploader.on('error', function(err) {
console.error("unable to upload:", err.stack);
});
uploader.on('progress', function() {
console.log("progress", uploader.progressMd5Amount,
uploader.progressAmount, uploader.progressTotal);
});
uploader.on('end', function() {
console.log("done uploading");
});
});
});
}
], function (err) {
if (err) {
console.error(
'Unable to resize ' + srcBucket + '/' + srcKey +
' and upload to ' + destBucket + '/' + destKey +
' due to an error: ' + err
);
} else {
console.log(
'Successfully resized ' + srcBucket + '/' + srcKey +
' and uploaded to ' + destBucket + '/' + destKey
);
}
}
);
};
My intention is to have a Lambda function on every insert of the image into s3 bucket and use convert command from imagemagick for optimisation. If I use convert command in command line , image quality is good. But, if I use the convert command in Lambda function, image quality is bad. Could you help to solve the problem?

s3 bucket upload not working

var async = require('async');
var AWS = require('aws-sdk');
var util = require('util');
var im = require('imagemagick');
var fs = require('fs');
// constants
var MAX_WIDTH = 100;
var MAX_HEIGHT = 100;
var s3 = require('s3');
var client = s3.createClient({
maxAsyncS3: 20, // this is the default
s3RetryCount: 3, // this is the default
s3RetryDelay: 1000, // this is the default
multipartUploadThreshold: 20971520, // this is the default (20 MB)
multipartUploadSize: 15728640, // this is the default (15 MB)
s3Options: {
accessKeyId: "xx",
secretAccessKey: "xx",
},
});
exports.handler = function(event, context, callback) {
// Read options from the event.
console.log("Reading options from event:\n", util.inspect(event, {depth: 5}));
var srcBucket = event.Records[0].s3.bucket.name;
// Object key may have spaces or unicode non-ASCII characters.
var srcKey = decodeURIComponent(event.Records[0].s3.object.key.replace(/\+/g, " "));
var dstBucket = srcBucket + "resized";
var dstKey = "resized-" + srcKey;
// Sanity check: validate that source and destination are different buckets.
if (srcBucket == dstBucket) {
callback("Source and destination buckets are the same.");
return;
}
// Infer the image type.
var typeMatch = srcKey.match(/\.([^.]*)$/);
if (!typeMatch) {
callback("Could not determine the image type.");
return;
}
var imageType = typeMatch[1];
if (imageType != "jpg" && imageType != "png") {
callback('Unsupported image type: ${imageType}');
return;
}
// Download the image from S3, transform, and upload to a different S3 bucket.
async.waterfall([
function download(next) {
var params = {
localFile: "/tmp/"+srcKey,
s3Params: {
Bucket: srcBucket,
Key: srcKey,
},
};
var downloader = client.downloadFile(params);
downloader.on('error', function(err) {
console.error("unable to download:", err.stack);
});
downloader.on('progress', function() {
console.log("progress", downloader.progressAmount, downloader.progressTotal);
});
downloader.on('end', function() {
console.log("done downloading");
});
//upload a file
var uploadparams = {
localFile: "/tmp/"+srcKey,
s3Params: {
Bucket: dstBucket,
Key: dstKey,
},
};
var uploader = client.uploadFile(uploadparams);
uploader.on('error', function(err) {
console.error("unable to upload:", err.stack);
});
uploader.on('progress', function() {
console.log("progress", uploader.progressMd5Amount,
uploader.progressAmount, uploader.progressTotal);
});
uploader.on('end', function() {
console.log("done uploading");
});
}
], function (err) {
if (err) {
console.error(
'Unable to resize ' + srcBucket + '/' + srcKey +
' and upload to ' + destBucket + '/' + destKey +
' due to an error: ' + err
);
} else {
console.log(
'Successfully resized ' + srcBucket + '/' + srcKey +
' and uploaded to ' + destBucket + '/' + destKey
);
}
}
);
};
I am trying to download a file from s3 bucket and upload it to a different s3 bucket. I need to do some to other conversions before uploading. So, just want to try, downloading and uploading first. On execution , it says done downloading. But I am unable to upload the file. Not sure what the problem is. I have followed the advice from https://github.com/andrewrk/node-s3-client/blob/master/README.md
Uploading is not at all working. Can you please help. Thx.
You are trying to upload at the same time you are downloading...
You need to call upload inside downloader.on('end', method

Promises or Async with Node js

I have this large amount of code which gets an image from a S3 bucket, saves it to a temporary file on Lambda, resizes it to 4 different sizes, saves it into different folders according to size and them puts the images back into the s3 bucket also into different folders.
However when running on Lambda, I have to call context.done() at the end of the whole process otherwise the context will remain alive until Lambda times out.
So I need to call context.done() when upload returns for the last time.
Looking into the two options, async and promises, which would likely need less refactoring of my code to work?
// dependencies
var AWS = require('aws-sdk');
var gm = require('gm').subClass({ imageMagick: true });
var fs = require("fs");
// get reference to S3 client
var s3 = new AWS.S3();
var _800px = {
width: 800,
destinationPath: "large"
};
var _500px = {
width: 500,
destinationPath: "medium"
};
var _200px = {
width: 200,
destinationPath: "small"
};
var _45px = {
width: 45,
destinationPath: "thumbnail"
};
var _sizesArray = [_800px, _500px, _200px, _45px];
var len = _sizesArray.length;
module to be exported when in production
ports.AwsHandler = function(event, context) {
// Read options from the event.
var srcBucket = event.Records[0].s3.bucket.name;
var srcKey = event.Records[0].s3.object.key;
var dstnFolder = "/tmp";
// function to determine paths
function _filePath (directory, i) {
if ( directory === false ) {
return "dst/" + _sizesArray[i].destinationPath + "/" + srcKey;
} else if ( directory === true ) {
return dstnFolder + "/" + _sizesArray[i].destinationPath + "/" + srcKey;
}
};
for ( var i = 0; i<len; i++) {
fs.mkdir("/tmp" + "/" + _sizesArray[i].destinationPath, function (err) {
if (err) {
console.log(err);
}
});
};
// Infer the image type.
var typeMatch = srcKey.match(/\.([^.]*)$/);
if (!typeMatch) {
console.error('unable to infer image type for key ' + srcKey);
return;
};
var imageType = typeMatch[1];
if (imageType != "jpg" && imageType != "png") {
console.log('skipping non-image ' + srcKey);
return;
};
function download () {
s3.getObject({
Bucket: srcBucket,
Key: srcKey
},
function (err, response) {
if (err) {
console.error(err);
}
fs.writeFile("/tmp" + "/" + srcKey, response.Body, function (err) {
transform();
})
}
);
};
function transform () {
var _Key,
_Size;
for ( var i = 0; i<len; i++ ) {
// define path for image write
_Key = _filePath (true, i);
// define sizes to resize to
_Size = _sizesArray[i].width;
// resize images
gm("/tmp/" + srcKey)
.resize(_Size)
.write(_Key, function (err) {
if (err) {
return handle(err);
}
if (!err) {
// get the result of write
var readPath = this.outname;
var iniPath = this.outname.slice(4);
var writePath = "dst".concat(iniPath);
read(err, readPath, writePath, upload);
}
});
};
};
function read (err, readPath, writePath, callback) {
// read file from temp directory
fs.readFile(readPath, function (err, data) {
if (err) {
console.log("NO READY FILE FOR YOU!!!");
console.error(err);
}
callback(data, writePath);
});
};
function upload (data, path) {
// upload images to s3 bucket
s3.putObject({
Bucket: srcBucket,
Key: path,
Body: data,
ContentType: data.type
},
function (err) {
if (err) {
console.error(err);
}
console.log("Uploaded with success!");
});
}
download();
Take a look at how they use Q in this example.
Your code will end up very similar to
download()
.then(transform)
.then(read)
.then(upload)
.catch(function (error) {
// Handle any error from all above steps
console.error(error);
})
.done(function() {
console.log('Finished processing image');
context.done();
});
You could also take a look to async and use it as they show in this other example.

Node resize image and upload to AWS

I'm relatively new to node, and want to write a module that takes an image from an S3 bucket, resizes it and saves it to a temporary directory on Amazon's new Lambda service and then uploads the images back to the bucket.
When I run the code, none of my functions seem to be called (download, transform and upload). I am using tmp to create the temporary directory and graphicsMagick to resize the image.
What is wrong with my code?
I have defined the dependencies and the array outside of the module, because I have another which depends on these.
// dependencies
var AWS = require('aws-sdk');
var gm = require('gm').subClass({ imageMagick: true });
var fs = require("fs");
var tmp = require("tmp");
// get reference to S3 client
var s3 = new AWS.S3();
var _800px = {
width: 800,
destinationPath: "large"
};
var _500px = {
width: 500,
destinationPath: "medium"
};
var _200px = {
width: 200,
destinationPath: "small"
};
var _45px = {
width: 45,
destinationPath: "thumbnail"
};
var _sizesArray = [_800px, _500px, _200px, _45px];
var len = _sizesArray.length;
exports.AwsHandler = function(event) {
// Read options from the event.
var srcBucket = event.Records[0].s3.bucket.name;
var srcKey = event.Records[0].s3.object.key;
var dstnKey = srcKey;
// create temporary directory
var tmpobj = tmp.dirSync();
// function to determine paths
function _filePath (directory, i) {
if (!directory) {
return "dst/" + _sizesArray[i].destinationPath + "/" + dstnKey;
} else {
return directory + "/dst/" + _sizesArray[i].destinationPath + "/" + dstnKey;
}
};
// Infer the image type.
var typeMatch = srcKey.match(/\.([^.]*)$/);
if (!typeMatch) {
console.error('unable to infer image type for key ' + srcKey);
return;
};
var imageType = typeMatch[1];
if (imageType != "jpg" && imageType != "png") {
console.log('skipping non-image ' + srcKey);
return;
};
(function resizeImage () {
function download () {
console.log("started!");
s3.getObject({
Bucket: srcBucket,
Key: srcKey
},
function (err, response) {
if (err) {
console.error(err);
}
// call transform if successful
transform (response);
}
);
};
function transform (response) {
for ( var i = 0; i<len; i++ ) {
// define path for image write
var _Key = _filePath (tmpobj, i);
// resize images
gm(response.Body, srcKey)
.resize(_sizesArray[i].width)
.write(_Key, function (err) {
if (err) {
console.error(err);
}
upLoad ();
});
}
};
function upLoad () {
for ( var i = 0; i<len; i++ ) {
var readPath = _filePath (tmpobj, i);
var writePath = _filePath (i);
// read file from temp directory
fs.readFile(readPath, function (err, data) {
if (err) {
console.error(err);
}
// upload images to s3 bucket
s3.putObject({
Bucket: srcBucket,
Key: writePath,
Body: data,
ContentType: data.type
},
function (err) {
if (err) {
console.error(err);
}
console.log("Uploaded with success!");
});
})
}
// Manual cleanup of temporary directory
tmpobj.removeCallback();
};
}());
};
Here's a partial improvement, note the use of the async library. You will have issues in upLoad() because you are firing four asynchronous calls immediately (in the for loop) and there's no easy way to know when they are all done. (well, the easy way is to rewrite the function to use the async library, like async.forEach)
// dependencies
var AWS = require('aws-sdk');
var gm = require('gm').subClass({ imageMagick: true });
var fs = require("fs");
var tmp = require("tmp");
var async = require("async");
// get reference to S3 client
var s3 = new AWS.S3();
var _800px = {
width: 800,
destinationPath: "large"
};
var _500px = {
width: 500,
destinationPath: "medium"
};
var _200px = {
width: 200,
destinationPath: "small"
};
var _45px = {
width: 45,
destinationPath: "thumbnail"
};
var _sizesArray = [_800px, _500px, _200px, _45px];
var len = _sizesArray.length;
exports.AwsHandler = function(event) {
// Read options from the event.
var srcBucket = event.Records[0].s3.bucket.name;
var srcKey = event.Records[0].s3.object.key;
var dstnKey = srcKey;
// create temporary directory
var tmpobj = tmp.dirSync();
// function to determine paths
function _filePath (directory, i) {
if (!directory) {
return "dst/" + _sizesArray[i].destinationPath + "/" + dstnKey;
} else {
return directory + "/dst/" + _sizesArray[i].destinationPath + "/" + dstnKey;
}
};
// Infer the image type.
var typeMatch = srcKey.match(/\.([^.]*)$/);
if (!typeMatch) {
console.error('unable to infer image type for key ' + srcKey);
return;
};
var imageType = typeMatch[1];
if (imageType != "jpg" && imageType != "png") {
console.log('skipping non-image ' + srcKey);
return;
};
// Actually call resizeImage, the main pipeline function:
resizeImage(function(err){
// Done. Manual cleanup of temporary directory
tmpobj.removeCallback();
})
function resizeImage (callback) {
var s3obj = {
Bucket: srcBucket,
Key: srcKey
};
download(s3obj, function(response){
var gmConfigs = sizesArray.map(function(size, i){
return {
width: size.width
_Key: _filePath (tmpobj, i)
}
})
async.eachSeries(gmConfigs,
function(config, done){
transform(response, config.width, config._Key, done)
},
function(err){
if(err){
console.log(err);
} else {
upLoad();
// Further work is required to identify if all the uploads worked,
// and to know when to call callback() here
// callback();
}
})
})
}
function download (s3obj, callback) {
console.log("started!");
s3.getObject(s3obj, function (err, response) {
if (err) {
console.error(err);
}
// call transform if successful
callback(response);
});
};
function transform (response, width, _Key, callback) {
// resize images
gm(response.Body, srcKey)
.resize(width)
.write(_Key, function (err) {
if (err) {
console.error(err);
}
callback();
});
};
function upLoad () {
for ( var i = 0; i<len; i++ ) {
var readPath = _filePath (tmpobj, i);
var writePath = _filePath (i);
// read file from temp directory
fs.readFile(readPath, function (err, data) {
if (err) {
console.error(err);
}
// upload images to s3 bucket
s3.putObject({
Bucket: srcBucket,
Key: writePath,
Body: data,
ContentType: data.type
},
function (err) {
if (err) {
console.error(err);
}
console.log("Uploaded with success!");
});
})
}
};
};

Resources