mirror of https://gitgud.io/fatchan/gm
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
369 lines
8.9 KiB
369 lines
8.9 KiB
|
|
/**
|
|
* Module dependencies.
|
|
*/
|
|
|
|
var exec = require('child_process').exec;
|
|
var spawn = require('child_process').spawn;
|
|
var utils = require('./utils');
|
|
var debug = require('debug')('gm');
|
|
var series = require('array-series');
|
|
var through = require('through');
|
|
var streamToBuffer = require('stream-to-buffer');
|
|
|
|
/**
|
|
* Error messaging.
|
|
*/
|
|
|
|
var missingIM = '** Have you installed imageMagick? **\n';
|
|
var missingGM = '** Have you installed graphicsmagick? **\n';
|
|
var noBufferConcat = 'gm v1.9.0+ required node v0.8+. Please update your version of node, downgrade gm < 1.9, or do not use `bufferStream`.';
|
|
|
|
/**
|
|
* Extend proto
|
|
*/
|
|
|
|
module.exports = function (proto) {
|
|
|
|
function args (prop) {
|
|
return function args () {
|
|
var len = arguments.length;
|
|
var a = [];
|
|
var i = 0;
|
|
|
|
for (; i < len; ++i) {
|
|
a.push(arguments[i]);
|
|
}
|
|
|
|
this[prop] = this[prop].concat(a);
|
|
return this;
|
|
}
|
|
}
|
|
|
|
proto.in = args('_in');
|
|
proto.out = args('_out');
|
|
|
|
proto._preprocessor = [];
|
|
proto.preprocessor = args('_preprocessor');
|
|
|
|
/**
|
|
* Execute the command and write the image to the specified file name.
|
|
*
|
|
* @param {String} name
|
|
* @param {Function} callback
|
|
* @return {Object} gm
|
|
*/
|
|
|
|
proto.write = function write (name, callback) {
|
|
if (!callback) callback = name, name = null;
|
|
|
|
if ("function" !== typeof callback) {
|
|
throw new TypeError("gm().write() expects a callback function")
|
|
}
|
|
|
|
if (!name) {
|
|
return callback(TypeError("gm().write() expects a filename when writing new files"));
|
|
}
|
|
|
|
this.outname = name;
|
|
|
|
var self = this;
|
|
this._preprocess(function (err) {
|
|
if (err) return callback(err);
|
|
self._spawn(self.args(), true, callback);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Execute the command and return stdin and stderr
|
|
* ReadableStreams providing the image data.
|
|
* If no callback is passed, a "through" stream will be returned,
|
|
* and stdout will be piped through, otherwise the error will be passed.
|
|
*
|
|
* @param {String} format (optional)
|
|
* @param {Function} callback (optional)
|
|
* @return {Stream}
|
|
*/
|
|
|
|
proto.stream = function stream (format, callback) {
|
|
if (!callback && typeof format === 'function') {
|
|
callback = format;
|
|
format = null;
|
|
}
|
|
|
|
var throughStream;
|
|
|
|
if ("function" !== typeof callback) {
|
|
throughStream = through()
|
|
callback = function (err, stdout, stderr) {
|
|
if (err) throughStream.emit('error', err);
|
|
else stdout.pipe(throughStream);
|
|
}
|
|
}
|
|
|
|
if (format) {
|
|
format = format.split('.').pop();
|
|
this.outname = format + ":-";
|
|
}
|
|
|
|
var self = this;
|
|
this._preprocess(function (err) {
|
|
if (err) return callback(err);
|
|
return self._spawn(self.args(), false, callback);
|
|
});
|
|
|
|
return throughStream
|
|
}
|
|
|
|
/**
|
|
* Convenience function for `proto.stream`.
|
|
* Simply returns the buffer instead of the stream.
|
|
*
|
|
* @param {String} format (optional)
|
|
* @param {Function} callback
|
|
* @return {null}
|
|
*/
|
|
|
|
proto.toBuffer = function toBuffer (format, callback) {
|
|
if (!callback) callback = format, format = null;
|
|
|
|
if ("function" !== typeof callback) {
|
|
throw new Error('gm().toBuffer() expects a callback.');
|
|
}
|
|
|
|
return this.stream(format, function (err, stdout) {
|
|
if (err) return callback(err);
|
|
|
|
streamToBuffer(stdout, callback);
|
|
})
|
|
}
|
|
|
|
/**
|
|
* Run any preProcessor functions in series. Used by autoOrient.
|
|
*
|
|
* @param {Function} callback
|
|
* @return {Object} gm
|
|
*/
|
|
|
|
proto._preprocess = function _preprocess (callback) {
|
|
series(this._preprocessor, this, callback);
|
|
}
|
|
|
|
/**
|
|
* Execute the command, buffer input and output, return stdout and stderr buffers.
|
|
*
|
|
* @param {String} bin
|
|
* @param {Array} args
|
|
* @param {Function} callback
|
|
* @return {Object} gm
|
|
*/
|
|
|
|
proto._exec = function _exec (args, callback) {
|
|
return this._spawn(args, true, callback);
|
|
}
|
|
|
|
/**
|
|
* Execute the command with stdin, returning stdout and stderr streams or buffers.
|
|
* @param {String} bin
|
|
* @param {Array} args
|
|
* @param {ReadableStream} stream
|
|
* @param {Boolean} shouldBuffer
|
|
* @param {Function} callback
|
|
* @return {Object} gm
|
|
* @TODO refactor this mess
|
|
*/
|
|
|
|
proto._spawn = function _spawn (args, bufferOutput, callback) {
|
|
var bin = this._options.imageMagick
|
|
? args.shift()
|
|
: 'gm'
|
|
|
|
var proc = spawn(bin, args)
|
|
, cmd = bin + ' ' + args.map(utils.escape).join(' ')
|
|
, self = this
|
|
, err;
|
|
|
|
debug(cmd);
|
|
|
|
if (self.sourceBuffer || self.sourceStream) {
|
|
// handle streaming errors when gm/imageMagick is not installed
|
|
proc.stdin.on('error', handlerr);
|
|
proc.stdout.on('error', handlerr);
|
|
function handlerr (err) {
|
|
if ('EPIPE' == err.code && 'write' == err.syscall) {
|
|
err.message = missingBinMsg(self) + err.message;
|
|
}
|
|
cb(self, err);
|
|
}
|
|
}
|
|
|
|
if (self.sourceBuffer) {
|
|
proc.stdin.write(this.sourceBuffer);
|
|
proc.stdin.end();
|
|
} else if (self.sourceStream) {
|
|
|
|
if (!self.sourceStream.readable) {
|
|
err = new Error("gm().stream() or gm().write() with a non-readable stream.");
|
|
return cb(self, err);
|
|
}
|
|
|
|
self.sourceStream.pipe(proc.stdin);
|
|
|
|
// bufferStream
|
|
// We convert the input source from a stream to a buffer.
|
|
if (self.bufferStream && !this._buffering) {
|
|
if (!Buffer.concat) {
|
|
throw new Error(noBufferConcat);
|
|
}
|
|
|
|
// Incase there are multiple processes in parallel,
|
|
// we only need one
|
|
self._buffering = true;
|
|
|
|
streamToBuffer(self.sourceStream, function (err, buffer) {
|
|
self.sourceBuffer = buffer;
|
|
self.sourceStream = null; // The stream is now dead
|
|
})
|
|
}
|
|
}
|
|
|
|
// for _exec operations (identify() mostly), we also
|
|
// need to buffer the output stream before returning
|
|
if (bufferOutput) {
|
|
var stdout = ''
|
|
, stderr = ''
|
|
, onOut
|
|
, onErr
|
|
, onExit
|
|
|
|
proc.stdout.on('data', onOut = function (data) {
|
|
stdout += data;
|
|
});
|
|
|
|
proc.stderr.on('data', onErr = function (data) {
|
|
stderr += data;
|
|
});
|
|
|
|
proc.on('close', onExit = function (code, signal) {
|
|
if (code !== 0 || signal !== null) {
|
|
if (127 == code)
|
|
stderr += missingBinMsg(self);
|
|
err = new Error('Command failed: ' + stderr);
|
|
err.code = code;
|
|
err.signal = signal;
|
|
};
|
|
cb(self, err, stdout, stderr, cmd);
|
|
stdout = stderr = onOut = onErr = onExit = null;
|
|
});
|
|
} else {
|
|
cb(self, null, proc.stdout, proc.stderr, cmd);
|
|
}
|
|
|
|
return self;
|
|
|
|
function cb (self, err, stdout, stderr, cmd) {
|
|
if (cb.called) return;
|
|
cb.called = 1;
|
|
if (args[0] !== 'identify' && bin !== 'identify') {
|
|
self._in = [];
|
|
self._out = [];
|
|
}
|
|
callback.call(self, err, stdout, stderr, cmd);
|
|
}
|
|
}
|
|
|
|
function missingBinMsg (self) {
|
|
if (self._options.imageMagick) return missingIM;
|
|
return missingGM;
|
|
}
|
|
|
|
/**
|
|
* Returns arguments to be used in the command.
|
|
*
|
|
* @return {Array}
|
|
*/
|
|
|
|
proto.args = function args () {
|
|
var outname = this.outname || "-";
|
|
if (this._outputFormat) outname = this._outputFormat + ':' + outname;
|
|
|
|
return [].concat(
|
|
this._subCommand
|
|
, this._in
|
|
, this.src()
|
|
, this._out
|
|
, outname
|
|
).filter(Boolean); // remove falsey
|
|
}
|
|
|
|
/**
|
|
* Adds an img source formatter.
|
|
*
|
|
* `formatters` are passed an array of images which will be
|
|
* used as 'input' images for the command. Useful for methods
|
|
* like `.append()` where multiple source images may be used.
|
|
*
|
|
* @param {Function} formatter
|
|
* @return {gm} this
|
|
*/
|
|
|
|
proto.addSrcFormatter = function addSrcFormatter (formatter) {
|
|
if ('function' != typeof formatter)
|
|
throw new TypeError('sourceFormatter must be a function');
|
|
this._sourceFormatters || (this._sourceFormatters = []);
|
|
this._sourceFormatters.push(formatter);
|
|
return this;
|
|
}
|
|
|
|
/**
|
|
* Applies all _sourceFormatters
|
|
*
|
|
* @return {Array}
|
|
*/
|
|
|
|
proto.src = function src () {
|
|
var arr = [];
|
|
for (var i = 0; i < this._sourceFormatters.length; ++i) {
|
|
this._sourceFormatters[i].call(this, arr);
|
|
}
|
|
return arr;
|
|
}
|
|
|
|
/**
|
|
* Image types.
|
|
*/
|
|
|
|
var types = {
|
|
'jpg': /\.jpe?g$/i
|
|
, 'png' : /\.png$/i
|
|
, 'gif' : /\.gif$/i
|
|
, 'tiff': /\.tif?f$/i
|
|
, 'bmp' : /(?:\.bmp|\.dib)$/i
|
|
, 'webp': /\.webp$/i
|
|
};
|
|
|
|
types.jpeg = types.jpg;
|
|
types.tif = types.tiff;
|
|
types.dib = types.bmp;
|
|
|
|
/**
|
|
* Determine the type of source image.
|
|
*
|
|
* @param {String} type
|
|
* @return {Boolean}
|
|
* @example
|
|
* if (this.inputIs('png')) ...
|
|
*/
|
|
|
|
proto.inputIs = function inputIs (type) {
|
|
if (!type) return false;
|
|
|
|
var rgx = types[type];
|
|
if (!rgx) {
|
|
if ('.' !== type[0]) type = '.' + type;
|
|
rgx = new RegExp('\\' + type + '$', 'i');
|
|
}
|
|
|
|
return rgx.test(this.source);
|
|
}
|
|
}
|
|
|