Last active
December 13, 2016 14:04
-
-
Save fractos/2f7c0a05a2132016cded64bf728af388 to your computer and use it in GitHub Desktop.
AWS Lambda code for transforming a JPEG2000 source image from an S3 bucket event to multiple JPG thumbnails with a IIIF-compatible info.json blob that are all recorded to S3. This should be packaged as a zip file which contains a node_modules folder that has the "tmp", "async" and "gm" libraries installed.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// dependencies | |
var async = require('async'); | |
var AWS = require('aws-sdk'); | |
var gm = require('gm') | |
.subClass({ imageMagick: true }); // Enable ImageMagick integration. | |
var util = require('util'); | |
var fs = require('fs'); | |
// constants for bounding boxes | |
var widths = [1000, 800, 400, 200]; | |
var heights = [1000, 600, 300, 150]; | |
var originalWidth = 0; | |
var originalHeight = 0; | |
var thumbnailJobs = widths.length; | |
var thumbnailSizes = new Array(widths.length); | |
// 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."); | |
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 = "dlcs-thumbs"; | |
if(srcBucket === "dlcs-storage-dev") { | |
dstBucket = dstBucket + "-dev"; | |
} | |
// srcKey like 2/1/image-id.jp2 | |
// for base we want 2/1/image-id | |
var dstKeyBase = srcKey.substring(0, srcKey.lastIndexOf(".")); | |
console.log("srcKey: " + srcKey); | |
// Download the image from S3, transform, and upload to a different S3 bucket. | |
var file = fs.createWriteStream('/tmp/object.jp2'); | |
s3.getObject({ | |
Bucket: srcBucket, | |
Key: srcKey | |
}, function(err, data) { | |
if(err) { console.log('error while reading: ' + err); } | |
else { | |
fs.writeFile('/tmp/object.jp2', data.Body, function(err) { | |
if(err) { console.log('error while writing: ' + err); } | |
else { | |
console.log('got object'); | |
if(fileExists('/tmp/object.jp2')) { | |
console.log('got to thumbnail'); | |
console.log('widths=' + widths.length); | |
for(var i = 0; i < widths.length; i++) { | |
writeThumbnail(i, widths[i], heights[i], dstKeyBase, dstBucket, context); | |
} | |
} | |
} | |
}); | |
} | |
}); | |
}; | |
function writeThumbnail(index, requiredWidth, requiredHeight, dstKeyBase, dstBucket, context) { | |
console.log('writeThumbnail(' + requiredWidth + ', ' + requiredHeight + ', ' + dstKeyBase + ', ' + dstBucket +')'); | |
var readStream = fs.createReadStream('/tmp/object.jp2'); | |
gm(readStream, '/tmp/object.jp2') | |
.size({bufferStream: true}, function(err, size) { | |
originalWidth = size.width; | |
originalHeight = size.height; | |
var width; | |
var height; | |
if(originalWidth <= requiredWidth && originalHeight <= requiredHeight) { | |
width = originalWidth; | |
height = originalHeight; | |
} else { | |
var scale1 = (requiredWidth / originalWidth); | |
var scale2 = (requiredHeight / originalHeight); | |
var scale = Math.min(scale1, scale2); | |
width = Math.round(scale * originalWidth); | |
height = Math.round(scale * originalHeight); | |
} | |
thumbnailSizes[index] = { width: width, height: height }; | |
this.resize(width, height); | |
var dstKey = dstKeyBase + "/full/" + width + "," + height + "/0/default.jpg"; | |
var tmpFilename = '/tmp/object-' + width + 'x' + height + '.jpg'; | |
this.write(tmpFilename, function(err) { | |
if(err) { console.log('error while writing: ' + err); } | |
else { | |
console.log('written ' + tmpFilename); | |
if(fileExists(tmpFilename)) { | |
var readStream2 = fs.createReadStream(tmpFilename); | |
readStream2.on('open', function() { | |
s3.putObject({ | |
Bucket: dstBucket, | |
Key: dstKey, | |
Body: readStream2 | |
}, function(err, response) { | |
console.log('got to last callback for ' + tmpFilename + ': ' + dstKey); | |
if (err) { | |
console.log(err); | |
} else { | |
console.log('good for ' + dstKey); | |
closeContextIfFinished(context, dstBucket, dstKeyBase); | |
} | |
}); | |
}); | |
} | |
} | |
}); | |
}); | |
} | |
function closeContextIfFinished(context, dstBucket, dstKeyBase) { | |
console.log('reducing number of outstanding jobs (currently: ' + thumbnailJobs + ').'); | |
thumbnailJobs--; | |
if(thumbnailJobs <= 0) { | |
console.log('outstanding jobs <= zero. signalling end of context.'); | |
var result = {originalWidth: originalWidth, originalHeight: originalHeight, thumbnails: widths.length}; | |
for(var i = 0; i < widths.length; i++) { | |
result[i] = thumbnailSizes[i]; | |
} | |
writeInfoJson(dstBucket, dstKeyBase, function() { | |
context.succeed(result); | |
}); | |
} | |
} | |
function writeInfoJson(dstBucket, dstKeyBase, continuation) { | |
var s3ThumbsUri = 'http://' + dstBucket + '.s3.eu-west-1.amazonaws.com/' + dstKeyBase; | |
var thumbSizesSnippet = ''; | |
for(var i = 1; i < widths.length; i++) { // skip first set of dimensions | |
thumbSizesSnippet = thumbSizesSnippet + '{ "width": ' + thumbnailSizes[i].width + ', "height": ' + thumbnailSizes[i].height + ' }'; | |
if(i < widths.length - 1) { | |
thumbSizesSnippet = thumbSizesSnippet + ',\n'; | |
} | |
} | |
var fileContents = | |
'{\n' + | |
'"@context":"http://iiif.io/api/image/2/context.json",\n' + | |
'"@id":"' + s3ThumbsUri + '",\n' + | |
'"profile": [\n' + | |
'"http://iiif.io/api/image/2/level0.json",\n' + | |
'{\n' + | |
'"formats" : [ "jpg" ],\n' + | |
'"qualities" : [ "color" ],\n' + | |
'"supports" : [ "sizeByWhListed" ]\n' + | |
'}\n' + | |
'],\n' + | |
'"width" : ' + originalWidth + ',\n' + | |
'"height" : ' + originalHeight + ',\n' + | |
'"sizes" : [\n' + | |
thumbSizesSnippet + | |
']\n' + | |
'}'; | |
s3.putObject({ | |
Bucket: dstBucket, | |
Key: dstKeyBase + '/info.json', | |
Body: fileContents, | |
ContentType: 'application/json', | |
}, function(err, response) { | |
console.log('got to last callback for ' + dstKeyBase + '/info.json'); | |
if (err) { | |
console.log(err); | |
} else { | |
console.log('good for ' + dstKeyBase + '/info.json'); | |
continuation(); | |
} | |
}); | |
} | |
function fileExists(filename) { | |
try { | |
stats = fs.lstatSync(filename); | |
if(stats.isFile()) { | |
// yes it is | |
console.log('found ' + filename); | |
return true; | |
} | |
} | |
catch(e) { | |
// didn't exist at all | |
console.log('could not find ' + filename); | |
return false; | |
} | |
return false; | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
{ | |
"name": "DLCS-Lambdas", | |
"version": "1.0.0", | |
"description": "AWS Lambda functions for DLCS operations.", | |
"devDependencies": { | |
"async": "^1.3.0", | |
"gm": "^1.18.0", | |
"tmp": "^0.0.26" | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment