node-image-farmer
Version:
Image thumbnailing middleware based on connect-thumb. Adds ability to retrieve files from the filesystem.
214 lines (189 loc) • 9.18 kB
JavaScript
var mime = require('mime'),
fs = require('fs'),
gm = require('gm'),
lockFile = require('lockfile'),
debug = require('debug')('node-image-farmer:image-modify');
var Canvas = require('canvas');
var SmartCrop = require('smartcrop');
if(!Promise){
var Promise = require('es6-promise').Promise;
}
var imageModifier = {
modifyImage: function (processOptions, options, appConfig, isChildProcess){
if(!isChildProcess){ debug("Modifying image..."); }
return new Promise(function(resolve, reject){
var srcPath = processOptions.filepath;
var dstPath = processOptions.modifiedFilePath;
var lockFileOptions = {
wait: 1000,
stale: 10000,
retries: 3,
retryWait: 250
};
var fileType = mime.lookup(srcPath);
if(appConfig.allowedMimeTypes.indexOf(fileType) > -1){
try {
if(!isChildProcess){ debug("Trying to identify image..."); }
gm(srcPath).identify(function (err, imageData) {
if (err || !(imageData && imageData.size && imageData.size.width)) {
if(!isChildProcess){ debug("--Couldn't get image information", err); }
reject(err);
return;
}
var origWidth = imageData.size.width;
var origHeight = imageData.size.height;
if(!isChildProcess){ debug("Setting up with these options: ", JSON.stringify(options, null, 2)); }
//if we have no width or height defines, just output the full size image.
if(!options.width && !options.height){
options.width = origWidth;
options.height = origHeight;
}else if(!options.width){
//no width specified, calculate from height
options.width = imageModifier.ratioCalculator(options.height, origHeight, origWidth);
}else if(!options.height){
//no height specified, calculate from width
options.height = imageModifier.ratioCalculator(options.width, origWidth, origHeight);
}
if(!isChildProcess){ debug("options are now: ", options); }
var img = new Canvas.Image();
var canvasOptions = {}; canvasOptions.canvasFactory = function(w, h) { return new Canvas(w, h); };
img.src = fs.readFileSync(srcPath);
canvasOptions.width = options.width;
canvasOptions.height = options.height;
if(options.minScale){
canvasOptions.minScale = options.minScale;
}
canvasOptions.dstPath = dstPath;
if(!isChildProcess){ debug("Using smartCrop with ", canvasOptions); }
SmartCrop.crop(img, canvasOptions, function(result) {
if(!isChildProcess){ debug(JSON.stringify(result.topCrop, null, 2)); }
var rt = result.topCrop;
lockFile.lock(srcPath + ".lock", lockFileOptions, function(err) {
if (err) {
console.log(err);
return reject(err);
}
if(!isChildProcess){ debug("Resizing and writing to" + dstPath + "..."); }
if(!isChildProcess){ debug("Cropping to", rt.width, rt.height, rt.x, rt.y); }
if(!isChildProcess){ debug("Resizing to", options.width, options.height); }
if(!isChildProcess){ debug("Source path: "+srcPath); }
gm(srcPath)
.crop(rt.width, rt.height, rt.x, rt.y)
.resize(options.width, options.height, "!")
.gravity('Center')
.quality(options.quality)
.stream(function(err, stdout, stderr){
if(!isChildProcess){ debug("--unlocking file"); }
lockFile.unlock(srcPath + ".lock", function (unlockErr) {
if (unlockErr) {
if(!isChildProcess){ debug(unlockErr); }
reject(unlockErr);
} else {
if (!err) {
if(!isChildProcess){ debug("--Writing file stream to "+dstPath); }
var writeStream = fs.createWriteStream(dstPath);
stdout.pipe(writeStream);
writeStream.on('close', function () {
resolve(dstPath);
});
} else {
if(!isChildProcess){ debug("gm error", err); }
reject(err);
}
}
});
});
});
});
});
}catch(err){
if(!isChildProcess){ debug("gm error", err); }
reject(err);
}
}else{
var err = {
responseCode: 403,
message: "Incorrect Mime Type!"
};
if(!isChildProcess){ debug(err, fileType); }
reject(err);
}
});
},
modifyImageProcess: function(processOptions, options, appConfig){
return new Promise(function(resolve, reject){
var spawn = require('child_process').spawn;
var resolved = false;
var env = {
processOptions: JSON.stringify(processOptions),
options: JSON.stringify(options),
appConfig: JSON.stringify(appConfig)
};
var child = spawn('node',
[
__dirname+'/image-modifier.js',
'processOptions', env.processOptions,
'options', env.options,
'appConfig', env.appConfig
]
);
// Listen for stdout data
child.stdout.on('data', function (data) {
var out = (""+data).replace(/\n$/, '');
try {
out = JSON.parse(out);
}catch(err){}
if(out.dstPath){
resolved = true;
resolve(out.dstPath);
}else{
resolved = true;
reject(out);
}
});
// Listen for an exit event:
child.on('error', function (err) {
if(err){
reject(err);
}
});
// Listen for an exit event:
child.on('exit', function (exitCode) {
if(!resolved){
reject(exitCode);
}
});
});
},
/**
* Detect targetHeight for a proportional resizing, when only width is indicated.
*
* @param targetWidth
* @param origWidth
* @param origHeight
*/
ratioCalculator: function (targetKnown, origTargetKnownEquiv, origTargetUnknownEquiv) {
return origTargetUnknownEquiv * targetKnown / origTargetKnownEquiv;
}
};
//Check to see if we've been executed as a process
var runOptions = {};
for(var i = 0; i < process.argv.length; i++){
if(['processOptions', 'options', 'appConfig'].indexOf(process.argv[i]) > -1){
runOptions[process.argv[i]] = process.argv[i+1];
}
}
if(runOptions.processOptions){
var processOptions = JSON.parse(runOptions.processOptions);
var options = JSON.parse(runOptions.options);
var appConfig = JSON.parse(runOptions.appConfig);
imageModifier.modifyImage(processOptions, options, appConfig, true).then(function(dstPath){
console.log(JSON.stringify({ dstPath: dstPath }));
}).catch(function(err){
console.log(JSON.stringify({
responseCode: 404,
message: err
}));
});
}
module.exports = imageModifier;