imgr-le
Version:
Image resizing, cropping, optimisation and serving
322 lines (276 loc) • 10 kB
JavaScript
/**
* Module dependencies.
*/
var gm = require('gm')
, fs = require('fs')
, path = require('path')
, mkdirp = require('mkdirp')
, utils = require('./utils')
, imagesize = require('./size')
, imgr = require('./constants');
/**
* A stub for skipping optimisation. Replace this function
* with your own to enable image optimisation.
*
* @param {String} path
* @param {Function} callback
*/
function noOptimisation(path, callback) {
callback();
}
/**
* Default options.
*/
var default_options = {
orientation: imgr.CENTRE
, image_magick: false
, optimisation: noOptimisation
, gm_quality: 100
, crop_offset: 0
, filter: null
, interlace: null
, coalesce: true
, trace: function () {}
};
/**
* Create a new image converter.
*
* @param {String} image - the image to load
* @param {Object} options (optional)
*/
var Converter = exports.Converter = function (image, options) {
this.image = image;
this.options = utils.mergeDefaults(options, default_options);
this.gm = gm.subClass({ imageMagick: this.options.image_magick });
this.operation = {};
};
/**
* Resize an image to the specified width.
*
* @param {Number} width
* @return this
*/
Converter.prototype.resizeToWidth = function (width) {
this.operation.width = width;
return this;
};
/**
* Resize an image to the specified height.
*
* @param {Number} height
* @return this
*/
Converter.prototype.resizeToHeight = function (height) {
this.operation.height = height;
return this;
};
/**
* Resize an image by the specified factor, e.g. 0.5 would resize the image
* to be half the width and height that it was.
*
* @param {Number} factor
* @return this
*/
Converter.prototype.resizeByFactor = function (factor) {
this.operation.factor = factor;
return this;
};
/**
* Resize an image to an exact width and height using adaptive resizing.
* Crop the largest portion of the image with the same aspect ratio and
* then resize to the desired dimensions.
*
* @param {Number} width
* @param {Number} height
* @param {Number} orientation (optional)
* @return this
*/
Converter.prototype.adaptiveResize = function (width, height, orientation) {
this.operation.width = width;
this.operation.height = height;
this.operation.orientation = orientation || this.options.orientation;
return this;
};
/**
* Crop an image to the specified width and height, starting from the
* specified x and y point.
*
* @param {Number} width
* @param {Number} height
* @param {Number} x (optional)
* @param {Number} y (optional)
* @return this
*/
Converter.prototype.crop = function (width, height, x, y) {
this.operation.crop_width = width;
this.operation.crop_height = height;
this.operation.x = x || 0;
this.operation.y = y || 0;
return this;
};
/**
* Get an image size.
*
* @param {String} image
* @param {Function} callback
*/
Converter.prototype.size = function (image, callback) {
imagesize(image, callback);
};
/**
* Execute the pending conversion and save the resulting image to `output`.
*
* @param {String} output
* @param {Function} callback
*/
Converter.prototype.save = function (output, callback) {
var dir = path.dirname(output)
, image = this.gm(this.image)
, operation = this.operation
, self = this;
this.callback = callback;
//Create the output dir if it doesn't already exist
mkdirp(dir, function () {
//Skip resize/crop?
if (!operation.width && !operation.height && !operation.factor && !operation.crop_width) {
return self.copy(self.image, output, function (err) {
return self.finalise(err);
});
}
//Get the current image dimensions
self.size(self.image, function (err, size) {
if (err || !size) {
return self.finalise(err || 'Failed to obtain image dimensions');
}
//Adaptive resizing
if (operation.width && operation.height && typeof operation.crop_width === 'undefined') {
var original_ar = size.width / size.height
, new_ar = operation.width / operation.height
, crop_offset = 0;
if (new_ar < original_ar) {
operation.crop_width = Math.round(operation.width * size.height / operation.height);
if (self.options.crop_offset) {
crop_offset = size.width * self.options.crop_offset / 100;
}
if (operation.orientation === imgr.LEFT) {
operation.x = crop_offset;
if (operation.x + operation.crop_width > size.width) {
operation.x = size.width - operation.crop_width;
}
} else if (operation.orientation === imgr.CENTRE) {
operation.x = Math.round((size.width - operation.crop_width) / 2);
} else if (operation.orientation === imgr.RIGHT) {
operation.x = Math.max(size.width - operation.crop_width - crop_offset, 0);
}
} else if (new_ar > original_ar) {
operation.crop_height = Math.round(operation.height * size.width / operation.width);
if (self.options.crop_offset) {
crop_offset = size.height * self.options.crop_offset / 100;
}
if (operation.orientation === imgr.TOP) {
operation.y = crop_offset;
if (operation.y + operation.crop_height > size.height) {
operation.y = size.height - operation.crop_height;
}
} else if (operation.orientation === imgr.CENTRE) {
operation.y = Math.round((size.height - operation.crop_height) / 2);
} else if (operation.orientation === imgr.BOTTOM) {
operation.y = Math.max(size.height - operation.crop_height - crop_offset, 0);
}
}
}
//Coalesce images (when resizing animated gifs)
if (self.options.coalesce) {
image.coalesce();
}
//Apply the crop operation
if (operation.crop_width || operation.crop_height) {
operation.x = operation.x || 0;
operation.y = operation.y || 0;
operation.crop_width = operation.crop_width || (size.width - operation.x);
operation.crop_height = operation.crop_height || (size.height - operation.y);
image.crop(operation.crop_width, operation.crop_height, operation.x, operation.y);
image.repage(0, 0, 0, 0);
}
//Resize by a constant factor
if (operation.factor) {
operation.width = Math.round(size.width * operation.factor);
operation.height = Math.round(size.height * operation.factor);
}
image.quality(self.options.gm_quality);
//Use a custom resizing filter
if (self.options.filter) {
image.filter(self.options.filter);
}
//Specify the type of interlacing scheme
if (self.options.interlace) {
image.interlace(self.options.interlace);
}
//Fill in the missing dimension
if (!operation.width && operation.height) {
operation.width = Math.ceil(operation.height / size.height * size.width);
}
if (!operation.height && operation.width) {
operation.height = Math.ceil(operation.width / size.width * size.height);
}
//Apply the resize operation
if (operation.width || operation.height) {
image.resize(operation.width, operation.height);
}
//Save the image
image.write(output, function (err) {
self.options.trace('gm_convert');
self.image = output;
self.finalise(err, operation);
});
});
});
};
/**
* Copy a file.
*
* @param {String} src
* @param {String} dest
* @param {Function} callback
* @api private
*/
Converter.prototype.copy = function (src, dest, callback) {
var stream = fs.createReadStream(src);
this.options.trace('image_copy');
stream.pipe(fs.createWriteStream(dest));
stream.on('end', callback);
stream.on('error', callback);
};
/**
* Finalise the save() process.
*
* @param {Function} callback
* @api private
*/
Converter.prototype.finalise = function (err, operation) {
var callback = this.callback
, image = this.image
, optimise = this.options.optimisation;
//Reset the operation
this.operation = {};
delete this.image;
delete this.callback;
if (err || !optimise) {
if (typeof err === 'object' && err.message.indexOf('installed graphicsmagick') >= 0) {
err = 'Graphicsmagick is not installed';
}
return callback(err);
}
if (optimise.length === 2) {
optimise(image, callback);
} else if (optimise.length === 3) {
optimise(image, operation, callback);
}
};
/**
* Manage concurrency.
*/
utils.concurrent(Converter.prototype, 'size', 5);
utils.floodProtection(Converter.prototype, 'size');
utils.concurrent(Converter.prototype, 'save', 5);
utils.floodProtection(Converter.prototype, 'save');