UNPKG

imagemagick

Version:

A wrapper around the imagemagick cli

416 lines (377 loc) 12.9 kB
var childproc = require('child_process'), EventEmitter = require('events').EventEmitter; function exec2(file, args /*, options, callback */) { var options = { encoding: 'utf8' , timeout: 0 , maxBuffer: 500*1024 , killSignal: 'SIGKILL' , output: null }; var callback = arguments[arguments.length-1]; if ('function' != typeof callback) callback = null; if (typeof arguments[2] == 'object') { var keys = Object.keys(options); for (var i = 0; i < keys.length; i++) { var k = keys[i]; if (arguments[2][k] !== undefined) options[k] = arguments[2][k]; } } var child = childproc.spawn(file, args); var killed = false; var timedOut = false; var Wrapper = function(proc) { this.proc = proc; this.stderr = new Accumulator(); proc.emitter = new EventEmitter(); proc.on = proc.emitter.on.bind(proc.emitter); this.out = proc.emitter.emit.bind(proc.emitter, 'data'); this.err = this.stderr.out.bind(this.stderr); this.errCurrent = this.stderr.current.bind(this.stderr); }; Wrapper.prototype.finish = function(err) { this.proc.emitter.emit('end', err, this.errCurrent()); }; var Accumulator = function(cb) { this.stdout = {contents: ""}; this.stderr = {contents: ""}; this.callback = cb; var limitedWrite = function(stream) { return function(chunk) { stream.contents += chunk; if (!killed && stream.contents.length > options.maxBuffer) { child.kill(options.killSignal); killed = true; } }; }; this.out = limitedWrite(this.stdout); this.err = limitedWrite(this.stderr); }; Accumulator.prototype.current = function() { return this.stdout.contents; }; Accumulator.prototype.errCurrent = function() { return this.stderr.contents; }; Accumulator.prototype.finish = function(err) { this.callback(err, this.stdout.contents, this.stderr.contents); }; var std = callback ? new Accumulator(callback) : new Wrapper(child); var timeoutId; if (options.timeout > 0) { timeoutId = setTimeout(function () { if (!killed) { child.kill(options.killSignal); timedOut = true; killed = true; timeoutId = null; } }, options.timeout); } child.stdout.setEncoding(options.encoding); child.stderr.setEncoding(options.encoding); child.stdout.addListener("data", function (chunk) { std.out(chunk, options.encoding); }); child.stderr.addListener("data", function (chunk) { std.err(chunk, options.encoding); }); var version = process.versions.node.split('.'); child.addListener(version[0] == 0 && version[1] < 7 ? "exit" : "close", function (code, signal) { if (timeoutId) clearTimeout(timeoutId); if (code === 0 && signal === null) { std.finish(null); } else { var e = new Error("Command "+(timedOut ? "timed out" : "failed")+": " + std.errCurrent()); e.timedOut = timedOut; e.killed = killed; e.code = code; e.signal = signal; std.finish(e); } }); return child; }; function parseIdentify(input) { var lines = input.split("\n"), prop = {}, props = [prop], prevIndent = 0, indents = [indent], currentLine, comps, indent, i; lines.shift(); //drop first line (Image: name.jpg) for (i in lines) { currentLine = lines[i]; indent = currentLine.search(/\S/); if (indent >= 0) { comps = currentLine.split(': '); if (indent > prevIndent) indents.push(indent); while (indent < prevIndent && props.length) { indents.pop(); prop = props.pop(); prevIndent = indents[indents.length - 1]; } if (comps.length < 2) { props.push(prop); prop = prop[currentLine.split(':')[0].trim().toLowerCase()] = {}; } else { prop[comps[0].trim().toLowerCase()] = comps[1].trim() } prevIndent = indent; } } return prop; }; exports.identify = function(pathOrArgs, callback) { var isCustom = Array.isArray(pathOrArgs), isData, args = isCustom ? ([]).concat(pathOrArgs) : ['-verbose', pathOrArgs]; if (typeof args[args.length-1] === 'object') { isData = true; pathOrArgs = args[args.length-1]; args[args.length-1] = '-'; if (!pathOrArgs.data) throw new Error('first argument is missing the "data" member'); } else if (typeof pathOrArgs === 'function') { args[args.length-1] = '-'; callback = pathOrArgs; } var proc = exec2(exports.identify.path, args, {timeout:120000}, function(err, stdout, stderr) { var result, geometry; if (!err) { if (isCustom) { result = stdout; } else { result = parseIdentify(stdout); geometry = result['geometry'].split(/x/); result.format = result.format.match(/\S*/)[0] result.width = parseInt(geometry[0]); result.height = parseInt(geometry[1]); result.depth = parseInt(result.depth); if (result.quality !== undefined) result.quality = parseInt(result.quality) / 100; } } callback(err, result); }); if (isData) { if ('string' === typeof pathOrArgs.data) { proc.stdin.setEncoding('binary'); proc.stdin.write(pathOrArgs.data, 'binary'); proc.stdin.end(); } else { proc.stdin.end(pathOrArgs.data); } } return proc; } exports.identify.path = 'identify'; function ExifDate(value) { // YYYY:MM:DD HH:MM:SS -> Date(YYYY-MM-DD HH:MM:SS +0000) value = value.split(/ /); return new Date(value[0].replace(/:/g, '-')+' '+ value[1]+' +0000'); } function exifKeyName(k) { return k.replace(exifKeyName.RE, function(x){ if (x.length === 1) return x.toLowerCase(); else return x.substr(0,x.length-1).toLowerCase()+x.substr(x.length-1); }); } exifKeyName.RE = /^[A-Z]+/; var exifFieldConverters = { // Numbers bitsPerSample:Number, compression:Number, exifImageLength:Number, exifImageWidth:Number, exifOffset:Number, exposureProgram:Number, flash:Number, imageLength:Number, imageWidth:Number, isoSpeedRatings:Number, jpegInterchangeFormat:Number, jpegInterchangeFormatLength:Number, lightSource:Number, meteringMode:Number, orientation:Number, photometricInterpretation:Number, planarConfiguration:Number, resolutionUnit:Number, rowsPerStrip:Number, samplesPerPixel:Number, sensingMethod:Number, stripByteCounts:Number, subSecTime:Number, subSecTimeDigitized:Number, subSecTimeOriginal:Number, customRendered:Number, exposureMode:Number, focalLengthIn35mmFilm:Number, gainControl:Number, saturation:Number, sharpness:Number, subjectDistanceRange:Number, subSecTime:Number, subSecTimeDigitized:Number, subSecTimeOriginal:Number, whiteBalance:Number, sceneCaptureType:Number, // Dates dateTime:ExifDate, dateTimeDigitized:ExifDate, dateTimeOriginal:ExifDate }; exports.readMetadata = function(path, callback) { return exports.identify(['-format', '%[EXIF:*]', path], function(err, stdout) { var meta = {}; if (!err) { stdout.split(/\n/).forEach(function(line){ var eq_p = line.indexOf('='); if (eq_p === -1) return; var key = line.substr(0, eq_p).replace('/','-'), value = line.substr(eq_p+1).trim(), typekey = 'default'; var p = key.indexOf(':'); if (p !== -1) { typekey = key.substr(0, p); key = key.substr(p+1); if (typekey === 'exif') { key = exifKeyName(key); var converter = exifFieldConverters[key]; if (converter) value = converter(value); } } if (!(typekey in meta)) meta[typekey] = {key:value}; else meta[typekey][key] = value; }) } callback(err, meta); }); } exports.convert = function(args, timeout, callback) { var procopt = {encoding: 'binary'}; if (typeof timeout === 'function') { callback = timeout; timeout = 0; } else if (typeof timeout !== 'number') { timeout = 0; } if (timeout && (timeout = parseInt(timeout)) > 0 && !isNaN(timeout)) procopt.timeout = timeout; return exec2(exports.convert.path, args, procopt, callback); } exports.convert.path = 'convert'; var resizeCall = function(t, callback) { var proc = exports.convert(t.args, t.opt.timeout, callback); if (t.opt.srcPath.match(/-$/)) { if ('string' === typeof t.opt.srcData) { proc.stdin.setEncoding('binary'); proc.stdin.write(t.opt.srcData, 'binary'); proc.stdin.end(); } else { proc.stdin.end(t.opt.srcData); } } return proc; } exports.resize = function(options, callback) { var t = exports.resizeArgs(options); return resizeCall(t, callback) } exports.crop = function (options, callback) { if (typeof options !== 'object') throw new TypeError('First argument must be an object'); if (!options.srcPath && !options.srcData) throw new TypeError("No srcPath or data defined"); if (!options.height && !options.width) throw new TypeError("No width or height defined"); if (options.srcPath){ var args = options.srcPath; } else { var args = { data: options.srcData }; } exports.identify(args, function(err, meta) { if (err) return callback && callback(err); var t = exports.resizeArgs(options), ignoreArg = false, printNext = false, args = []; t.args.forEach(function (arg) { if (printNext === true){ console.log("arg", arg); printNext = false; } // ignoreArg is set when resize flag was found if (!ignoreArg && (arg != '-resize')) args.push(arg); // found resize flag! ignore the next argument if (arg == '-resize'){ console.log("resize arg"); ignoreArg = true; printNext = true; } if (arg === "-crop"){ console.log("crop arg"); printNext = true; } // found the argument after the resize flag; ignore it and set crop options if ((arg != "-resize") && ignoreArg) { var dSrc = meta.width / meta.height, dDst = t.opt.width / t.opt.height, resizeTo = (dSrc < dDst) ? ''+t.opt.width+'x' : 'x'+t.opt.height, dGravity = options.gravity ? options.gravity : "Center"; args = args.concat([ '-resize', resizeTo, '-gravity', dGravity, '-crop', ''+t.opt.width + 'x' + t.opt.height + '+0+0', '+repage' ]); ignoreArg = false; } }) t.args = args; resizeCall(t, callback); }) } exports.resizeArgs = function(options) { var opt = { srcPath: null, srcData: null, srcFormat: null, dstPath: null, quality: 0.8, format: 'jpg', progressive: false, colorspace: null, width: 0, height: 0, strip: true, filter: 'Lagrange', sharpening: 0.2, customArgs: [], timeout: 0 } // check options if (typeof options !== 'object') throw new Error('first argument must be an object'); for (var k in opt) if (k in options) opt[k] = options[k]; if (!opt.srcPath && !opt.srcData) throw new Error('both srcPath and srcData are empty'); // normalize options if (!opt.format) opt.format = 'jpg'; if (!opt.srcPath) { opt.srcPath = (opt.srcFormat ? opt.srcFormat +':-' : '-'); // stdin } if (!opt.dstPath) opt.dstPath = (opt.format ? opt.format+':-' : '-'); // stdout if (opt.width === 0 && opt.height === 0) throw new Error('both width and height can not be 0 (zero)'); // build args var args = [opt.srcPath]; if (opt.sharpening > 0) { args = args.concat([ '-set', 'option:filter:blur', String(1.0-opt.sharpening)]); } if (opt.filter) { args.push('-filter'); args.push(opt.filter); } if (opt.strip) { args.push('-strip'); } if (opt.width || opt.height) { args.push('-resize'); if (opt.height === 0) args.push(String(opt.width)); else if (opt.width === 0) args.push('x'+String(opt.height)); else args.push(String(opt.width)+'x'+String(opt.height)); } opt.format = opt.format.toLowerCase(); var isJPEG = (opt.format === 'jpg' || opt.format === 'jpeg'); if (isJPEG && opt.progressive) { args.push('-interlace'); args.push('plane'); } if (isJPEG || opt.format === 'png') { args.push('-quality'); args.push(Math.round(opt.quality * 100.0).toString()); } else if (opt.format === 'miff' || opt.format === 'mif') { args.push('-quality'); args.push(Math.round(opt.quality * 9.0).toString()); } if (opt.colorspace) { args.push('-colorspace'); args.push(opt.colorspace); } if (Array.isArray(opt.customArgs) && opt.customArgs.length) args = args.concat(opt.customArgs); args.push(opt.dstPath); return {opt:opt, args:args}; }