UNPKG

pixelworks

Version:

Utilities for working with ImageData in Workers

275 lines (257 loc) 7.98 kB
var newImageData = require('./util').newImageData; /** * Create a function for running operations. This function is serialized for * use in a worker. * @param {function(Array, Object):*} operation The operation. * @return {function(Object):ArrayBuffer} A function that takes an object with * buffers, meta, imageOps, width, and height properties and returns an array * buffer. */ function createMinion(operation) { var workerHasImageData = true; try { new ImageData(10, 10); } catch (_) { workerHasImageData = false; } function newWorkerImageData(data, width, height) { if (workerHasImageData) { return new ImageData(data, width, height); } else { return {data: data, width: width, height: height}; } } return function(data) { // bracket notation for minification support var buffers = data['buffers']; var meta = data['meta']; var imageOps = data['imageOps']; var width = data['width']; var height = data['height']; var numBuffers = buffers.length; var numBytes = buffers[0].byteLength; var output, b; if (imageOps) { var images = new Array(numBuffers); for (b = 0; b < numBuffers; ++b) { images[b] = newWorkerImageData( new Uint8ClampedArray(buffers[b]), width, height); } output = operation(images, meta).data; } else { output = new Uint8ClampedArray(numBytes); var arrays = new Array(numBuffers); var pixels = new Array(numBuffers); for (b = 0; b < numBuffers; ++b) { arrays[b] = new Uint8ClampedArray(buffers[b]); pixels[b] = [0, 0, 0, 0]; } for (var i = 0; i < numBytes; i += 4) { for (var j = 0; j < numBuffers; ++j) { var array = arrays[j]; pixels[j][0] = array[i]; pixels[j][1] = array[i + 1]; pixels[j][2] = array[i + 2]; pixels[j][3] = array[i + 3]; } var pixel = operation(pixels, meta); output[i] = pixel[0]; output[i + 1] = pixel[1]; output[i + 2] = pixel[2]; output[i + 3] = pixel[3]; } } return output.buffer; }; } /** * Create a worker for running operations. * @param {Object} config Configuration. * @param {function(MessageEvent)} onMessage Called with a message event. * @return {Worker} The worker. */ function createWorker(config, onMessage) { var lib = Object.keys(config.lib || {}).map(function(name) { return 'var ' + name + ' = ' + config.lib[name].toString() + ';'; }); var lines = lib.concat([ 'var __minion__ = (' + createMinion.toString() + ')(', config.operation.toString(), ');', 'self.addEventListener("message", function(event) {', ' var buffer = __minion__(event.data);', ' self.postMessage({buffer: buffer, meta: event.data.meta}, [buffer]);', '});' ]); var blob = new Blob(lines, {type: 'text/javascript'}); var source = URL.createObjectURL(blob); var worker = new Worker(source); worker.addEventListener('message', onMessage); return worker; } /** * Create a faux worker for running operations. * @param {Object} config Configuration. * @param {function(MessageEvent)} onMessage Called with a message event. * @return {Object} The faux worker. */ function createFauxWorker(config, onMessage) { var minion = createMinion(config.operation); return { postMessage: function(data) { setTimeout(function() { onMessage({'data': {'buffer': minion(data), 'meta': data['meta']}}); }, 0); } }; } /** * A processor runs pixel or image operations in workers. * @param {Object} config Configuration. */ function Processor(config) { this._imageOps = !!config.imageOps; var threads; if (config.threads === 0) { threads = 0; } else if (this._imageOps) { threads = 1; } else { threads = config.threads || 1; } var workers = []; if (threads) { for (var i = 0; i < threads; ++i) { workers[i] = createWorker(config, this._onWorkerMessage.bind(this, i)); } } else { workers[0] = createFauxWorker(config, this._onWorkerMessage.bind(this, 0)); } this._workers = workers; this._queue = []; this._maxQueueLength = config.queue || Infinity; this._running = 0; this._dataLookup = {}; this._job = null; } /** * Run operation on input data. * @param {Array.<Array|ImageData>} inputs Array of pixels or image data * (depending on the operation type). * @param {Object} meta A user data object. This is passed to all operations * and must be serializable. * @param {function(Error, ImageData, Object)} callback Called when work * completes. The first argument is any error. The second is the ImageData * generated by operations. The third is the user data object. */ Processor.prototype.process = function(inputs, meta, callback) { this._enqueue({ inputs: inputs, meta: meta, callback: callback }); this._dispatch(); }; /** * Stop responding to any completed work and destroy the processor. */ Processor.prototype.destroy = function() { for (var key in this) { this[key] = null; } this._destroyed = true; }; /** * Add a job to the queue. * @param {Object} job The job. */ Processor.prototype._enqueue = function(job) { this._queue.push(job); while (this._queue.length > this._maxQueueLength) { this._queue.shift().callback(null, null); } }; /** * Dispatch a job. */ Processor.prototype._dispatch = function() { if (this._running === 0 && this._queue.length > 0) { var job = this._job = this._queue.shift(); var width = job.inputs[0].width; var height = job.inputs[0].height; var buffers = job.inputs.map(function(input) { return input.data.buffer; }); var threads = this._workers.length; this._running = threads; if (threads === 1) { this._workers[0].postMessage({ 'buffers': buffers, 'meta': job.meta, 'imageOps': this._imageOps, 'width': width, 'height': height }, buffers); } else { var length = job.inputs[0].data.length; var segmentLength = 4 * Math.ceil(length / 4 / threads); for (var i = 0; i < threads; ++i) { var offset = i * segmentLength; var slices = []; for (var j = 0, jj = buffers.length; j < jj; ++j) { slices.push(buffers[i].slice(offset, offset + segmentLength)); } this._workers[i].postMessage({ 'buffers': slices, 'meta': job.meta, 'imageOps': this._imageOps, 'width': width, 'height': height }, slices); } } } }; /** * Handle messages from the worker. * @param {number} index The worker index. * @param {MessageEvent} event The message event. */ Processor.prototype._onWorkerMessage = function(index, event) { if (this._destroyed) { return; } this._dataLookup[index] = event.data; --this._running; if (this._running === 0) { this._resolveJob(); } }; /** * Resolve a job. If there are no more worker threads, the processor callback * will be called. */ Processor.prototype._resolveJob = function() { var job = this._job; var threads = this._workers.length; var data, meta; if (threads === 1) { data = new Uint8ClampedArray(this._dataLookup[0]['buffer']); meta = this._dataLookup[0]['meta']; } else { var length = job.inputs[0].data.length; data = new Uint8ClampedArray(length); meta = new Array(length); var segmentLength = 4 * Math.ceil(length / 4 / threads); for (var i = 0; i < threads; ++i) { var buffer = this._dataLookup[i]['buffer']; var offset = i * segmentLength; data.set(new Uint8ClampedArray(buffer), offset); meta[i] = this._dataLookup[i]['meta']; } } this._job = null; this._dataLookup = {}; job.callback(null, newImageData(data, job.inputs[0].width, job.inputs[0].height), meta); this._dispatch(); }; module.exports = Processor;