UNPKG

graphicsmagick-stream

Version:

Fast convertion/scaling of images using a pool of long lived graphicsmagick processes

260 lines (211 loc) 6.4 kB
var proc = require('child_process') var once = require('once') var xtend = require('xtend') var Duplex = require('stream').Duplex var path = require('path') var noop = function() {} var EMPTY = new Buffer(0) var FORMATS = ['noop', 'info', 'jpeg', 'gif', 'png', 'bmp', 'pdf'] var DEBUGGING = process.env.DEBUG && /graphicsmagick-stream|\*/.test(process.env.DEBUG) var toFormatType = function(format) { if (!format) return 0 switch (format.toLowerCase()) { case 'info': return 1 case 'jpeg': case 'jpg': return 2 case 'gif': return 3 case 'png': return 4 case 'bmp': return 5 case 'pdf': return 6 } return 0 } var fromInfoStruct = function(data) { var result = {} result.width = data.readUInt32LE(0) result.height = data.readUInt32LE(4) result.format = FORMATS[data.readUInt32LE(8)] result.pages = data.readUInt32LE(12) return result } var toUInt32LE = function(len) { var buf = new Buffer(4) buf.writeUInt32LE(len, 0) return buf } var toStruct = function(opts) { var buf = new Buffer(52) var offset = -4 // scale if (typeof opts.scale === 'number') opts.scale = {width:opts.scale, height:opts.scale} var scale = opts.scale || {} var scaleOpts = 0 if (scale.type === 'fixed') scaleOpts |= 2 else if (scale.type === 'cover') scaleOpts |= 4 else scaleOpts |= 1 if (scale.multipage) scaleOpts |= 8 buf.writeUInt32LE(scaleOpts, offset += 4) buf.writeUInt32LE(scale.width || 0, offset += 4) buf.writeUInt32LE(scale.height || 0, offset += 4) // crop if (typeof opts.crop === 'number') opts.crop = {width:opts.crop, height:opts.crop} var crop = opts.crop || {} buf.writeUInt32LE(crop.x || 0, offset += 4) buf.writeUInt32LE(crop.y || 0, offset += 4) buf.writeUInt32LE(crop.width || 0, offset += 4) buf.writeUInt32LE(crop.height || 0, offset += 4) // rotate var degrees = opts.rotate === 'auto' || opts.rotate === true ? 360 : (opts.rotate || 0) buf.writeUInt32LE(degrees < 0 ? 360 + degrees : degrees, offset += 4) // density buf.writeUInt32LE(opts.density || 0, offset += 4) // pages var page = typeof opts.page === 'number' ? [opts.page, opts.page] : opts.page || [0,0] buf.writeUInt32LE(page[0] || 0, offset += 4) buf.writeUInt32LE(page[1] || 0, offset += 4) // output format buf.writeUInt32LE(toFormatType(opts.format), offset += 4) // split buf.writeUInt32LE(opts.split || 0, offset += 4) return buf } var destroyer = function(stream) { var destroyed = false return function(err) { if (destroyed) return if (err) stream.emit('error', err) destroyed = true stream.emit('close') } } var pool = function(opts) { if (!opts) opts = {} var size = opts.size || 1 var workers = [] for (var i = 0; i < size; i++) workers[i] = {queue:[], process:null} var update = function(worker) { if (!worker.process) return if (worker.queue.length) { worker.process.ref() worker.process.stdout.ref() worker.process.stderr.ref() worker.process.stdin.ref() } else { worker.process.unref() worker.process.stdout.unref() worker.process.stderr.unref() worker.process.stdin.unref() } } var select = function() { var worker = workers.reduce(function(a, b) { return a.queue.length <= b.queue.length ? a : b }) if (worker.process) return worker var child = worker.process = proc.spawn(path.join(__dirname, 'bin/convert')) var onerror = once(function(err) { child.kill() }) child.on('exit', function(code) { var err = new Error('graphicsmagick crashed with code: '+code) if (stream) stream.destroy(err) while (worker.queue.length) worker.queue.shift().destroy(err) worker.process = null }) child.stdout.on('error', onerror) child.stderr.on('error', onerror) child.stdin.on('error', onerror) if (DEBUGGING) child.stderr.pipe(process.stderr) var missing = 0 var stream var draining = false var drain = function() { if (draining) return draining = true while (true) { if (!missing) { var buf = child.stdout.read(4) if (!buf) break missing = buf.readUInt32LE(0) stream = worker.queue[0] stream._read = drain } var block = child.stdout.read(Math.min(missing, child.stdout._readableState.length)) if (!block) break missing -= block.length var drained = stream.push(block) if (!missing) { worker.queue.shift() stream.push(null) stream._read = noop if (worker.queue[0]) worker.queue[0].kick() update(worker) } if (!drained) break } draining = false } child.stdout.on('readable', drain) return worker } return function(opts) { var worker = select() var dup = new Duplex() var buffer = [toStruct(opts)] var destroyed = false var wait dup.on('finish', function() { buffer = Buffer.concat(buffer) worker.process.stdin.write(toUInt32LE(buffer.length)) worker.process.stdin.write(buffer) }) dup._read = noop dup._write = function(data, enc, cb) { if (worker.queue[0] !== dup) { wait = [data, cb] return } buffer.push(data) cb() } dup.kick = function() { var w = wait wait = null if (w) dup._write(w[0], null, w[1]) } dup.destroy = function(err) { if (destroyed) return destroyed = true if (err) dup.emit('error', err) dup.emit('close') } worker.queue.push(dup) update(worker) return dup } } module.exports = function(defaults) { if (typeof defaults === 'number') defaults = {pool:defaults} if (!defaults) defaults = {} var size = defaults.pool || 1 var exec = pool({size:size}) var convert = function(opts) { return exec(xtend(defaults, opts)) } convert.info = function(opts, cb) { if (typeof opts === 'function') return convert.info(null, opts) if (!opts) opts = {} opts.format = 'info' cb = once(cb) var buf = [] return convert(opts) .on('error', cb) .on('data', function(data) { buf.push(data) }) .on('end', function() { cb(null, fromInfoStruct(Buffer.concat(buf))) }) } return convert }