├── .github
└── workflows
│ └── test.yml
├── .gitignore
├── examples
├── 0.jpg
├── operations.html
└── operations.js
├── lib
├── index.js
├── processor.js
└── util.js
├── license.md
├── package-lock.json
├── package.json
├── readme.md
└── test
├── karma.conf.js
└── lib
└── processor.test.js
/.github/workflows/test.yml:
--------------------------------------------------------------------------------
1 | name: Test
2 |
3 | on:
4 | push:
5 | branches:
6 | - main
7 | pull_request:
8 | branches:
9 | - main
10 |
11 | env:
12 | CI: true
13 |
14 | jobs:
15 | build:
16 | name: Test
17 | runs-on: ubuntu-latest
18 |
19 | steps:
20 | - uses: actions/checkout@v2
21 |
22 | - uses: actions/setup-node@v2
23 | with:
24 | node-version: 16
25 |
26 | - run: npm ci
27 | - run: npm test
28 | - run: npm run build
29 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /node_modules/
2 | /dist/
3 |
--------------------------------------------------------------------------------
/examples/0.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tschaub/pixelworks/993605da0f9f88ad4d4d27c698491e68da48db78/examples/0.jpg
--------------------------------------------------------------------------------
/examples/operations.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | Pixel Operations
4 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
--------------------------------------------------------------------------------
/examples/operations.js:
--------------------------------------------------------------------------------
1 | /* global pixelworks */
2 |
3 | const luminance = function(pixels) {
4 | const pixel = pixels[0];
5 | const l = 0.2126 * pixel[0] + 0.7152 * pixel[1] + 0.0722 * pixel[2];
6 | pixel[0] = l;
7 | pixel[1] = l;
8 | pixel[2] = l;
9 | return pixels;
10 | };
11 |
12 | const color = function(pixels, data) {
13 | const pixel = pixels[0];
14 | const l = pixel[0];
15 | if (l > data.threshold) {
16 | pixel[0] = 255;
17 | pixel[1] = 255;
18 | pixel[2] = 150;
19 | pixel[3] = 200;
20 | } else {
21 | pixel[3] = 0;
22 | }
23 | return pixel;
24 | };
25 |
26 | const inputContext = document.getElementById('input').getContext('2d');
27 | const outputContext = document.getElementById('output').getContext('2d');
28 | const image = new Image();
29 |
30 | const worker = new pixelworks.Processor({
31 | operation: function(pixels, data) {
32 | return color(luminance(pixels), data);
33 | },
34 | lib: {
35 | luminance: luminance,
36 | color: color
37 | }
38 | });
39 |
40 | const threshold = document.getElementById('threshold');
41 | const data = {
42 | threshold: threshold.value
43 | };
44 |
45 | function process() {
46 | const canvas = inputContext.canvas;
47 | const input = inputContext.getImageData(0, 0, canvas.width, canvas.height);
48 | worker.process([input], data, function(err, output) {
49 | if (err) {
50 | throw err;
51 | }
52 | outputContext.putImageData(output, 0, 0);
53 | });
54 | }
55 |
56 | threshold.oninput = function() {
57 | data.threshold = this.value;
58 | process();
59 | };
60 |
61 | image.onload = function() {
62 | inputContext.drawImage(image, 0, 0);
63 | process();
64 | };
65 |
66 | image.src = '0.jpg';
67 |
--------------------------------------------------------------------------------
/lib/index.js:
--------------------------------------------------------------------------------
1 | const Processor = require('./processor');
2 |
3 | exports.Processor = Processor;
4 |
--------------------------------------------------------------------------------
/lib/processor.js:
--------------------------------------------------------------------------------
1 | const newImageData = require('./util').newImageData;
2 |
3 | /**
4 | * Create a function for running operations. This function is serialized for
5 | * use in a worker.
6 | * @param {function(Array, Object):*} operation The operation.
7 | * @return {function(Object):ArrayBuffer} A function that takes an object with
8 | * buffers, meta, imageOps, width, and height properties and returns an array
9 | * buffer.
10 | */
11 | function createMinion(operation) {
12 | let workerHasImageData = true;
13 | try {
14 | new ImageData(10, 10);
15 | } catch (_) {
16 | workerHasImageData = false;
17 | }
18 |
19 | function newWorkerImageData(data, width, height) {
20 | if (workerHasImageData) {
21 | return new ImageData(data, width, height);
22 | } else {
23 | return {data: data, width: width, height: height};
24 | }
25 | }
26 |
27 | return function(data) {
28 | // bracket notation for minification support
29 | const buffers = data['buffers'];
30 | const meta = data['meta'];
31 | const imageOps = data['imageOps'];
32 | const width = data['width'];
33 | const height = data['height'];
34 |
35 | const numBuffers = buffers.length;
36 | const numBytes = buffers[0].byteLength;
37 | let output, b;
38 |
39 | if (imageOps) {
40 | const images = new Array(numBuffers);
41 | for (b = 0; b < numBuffers; ++b) {
42 | images[b] = newWorkerImageData(
43 | new Uint8ClampedArray(buffers[b]),
44 | width,
45 | height
46 | );
47 | }
48 | output = operation(images, meta).data;
49 | } else {
50 | output = new Uint8ClampedArray(numBytes);
51 | const arrays = new Array(numBuffers);
52 | const pixels = new Array(numBuffers);
53 | for (b = 0; b < numBuffers; ++b) {
54 | arrays[b] = new Uint8ClampedArray(buffers[b]);
55 | pixels[b] = [0, 0, 0, 0];
56 | }
57 | for (let i = 0; i < numBytes; i += 4) {
58 | for (let j = 0; j < numBuffers; ++j) {
59 | const array = arrays[j];
60 | pixels[j][0] = array[i];
61 | pixels[j][1] = array[i + 1];
62 | pixels[j][2] = array[i + 2];
63 | pixels[j][3] = array[i + 3];
64 | }
65 | const pixel = operation(pixels, meta);
66 | output[i] = pixel[0];
67 | output[i + 1] = pixel[1];
68 | output[i + 2] = pixel[2];
69 | output[i + 3] = pixel[3];
70 | }
71 | }
72 | return output.buffer;
73 | };
74 | }
75 |
76 | /**
77 | * Create a worker for running operations.
78 | * @param {Object} config Configuration.
79 | * @param {function(MessageEvent)} onMessage Called with a message event.
80 | * @return {Worker} The worker.
81 | */
82 | function createWorker(config, onMessage) {
83 | const lib = Object.keys(config.lib || {}).map(function(name) {
84 | return 'var ' + name + ' = ' + config.lib[name].toString() + ';';
85 | });
86 |
87 | const lines = lib.concat([
88 | 'var __minion__ = (' + createMinion.toString() + ')(',
89 | config.operation.toString(),
90 | ');',
91 | 'self.addEventListener("message", function(event) {',
92 | ' var buffer = __minion__(event.data);',
93 | ' self.postMessage({buffer: buffer, meta: event.data.meta}, [buffer]);',
94 | '});'
95 | ]);
96 |
97 | const blob = new Blob(lines, {type: 'text/javascript'});
98 | const source = URL.createObjectURL(blob);
99 | const worker = new Worker(source);
100 | worker.addEventListener('message', onMessage);
101 | return worker;
102 | }
103 |
104 | /**
105 | * Create a faux worker for running operations.
106 | * @param {Object} config Configuration.
107 | * @param {function(MessageEvent)} onMessage Called with a message event.
108 | * @return {Object} The faux worker.
109 | */
110 | function createFauxWorker(config, onMessage) {
111 | const minion = createMinion(config.operation);
112 | let terminated = false;
113 | return {
114 | postMessage: function(data) {
115 | setTimeout(function() {
116 | if (terminated) {
117 | return;
118 | }
119 | onMessage({data: {buffer: minion(data), meta: data['meta']}});
120 | }, 0);
121 | },
122 | terminate: function() {
123 | terminated = true;
124 | }
125 | };
126 | }
127 |
128 | /**
129 | * A processor runs pixel or image operations in workers.
130 | * @param {Object} config Configuration.
131 | */
132 | function Processor(config) {
133 | this._imageOps = !!config.imageOps;
134 | let threads;
135 | if (config.threads === 0) {
136 | threads = 0;
137 | } else if (this._imageOps) {
138 | threads = 1;
139 | } else {
140 | threads = config.threads || 1;
141 | }
142 | const workers = [];
143 | if (threads) {
144 | for (let i = 0; i < threads; ++i) {
145 | workers[i] = createWorker(config, this._onWorkerMessage.bind(this, i));
146 | }
147 | } else {
148 | workers[0] = createFauxWorker(config, this._onWorkerMessage.bind(this, 0));
149 | }
150 | this._workers = workers;
151 | this._queue = [];
152 | this._maxQueueLength = config.queue || Infinity;
153 | this._running = 0;
154 | this._dataLookup = {};
155 | this._job = null;
156 | }
157 |
158 | /**
159 | * Run operation on input data.
160 | * @param {Array.} inputs Array of pixels or image data
161 | * (depending on the operation type).
162 | * @param {Object} meta A user data object. This is passed to all operations
163 | * and must be serializable.
164 | * @param {function(Error, ImageData, Object)} callback Called when work
165 | * completes. The first argument is any error. The second is the ImageData
166 | * generated by operations. The third is the user data object.
167 | */
168 | Processor.prototype.process = function(inputs, meta, callback) {
169 | this._enqueue({
170 | inputs: inputs,
171 | meta: meta,
172 | callback: callback
173 | });
174 | this._dispatch();
175 | };
176 |
177 | /**
178 | * Stop responding to any completed work and destroy the processor.
179 | */
180 | Processor.prototype.destroy = function() {
181 | for (let i = 0; i < this._workers.length; ++i) {
182 | this._workers[i].terminate();
183 | }
184 | for (const key in this) {
185 | this[key] = null;
186 | }
187 | this._destroyed = true;
188 | };
189 |
190 | /**
191 | * Add a job to the queue.
192 | * @param {Object} job The job.
193 | */
194 | Processor.prototype._enqueue = function(job) {
195 | this._queue.push(job);
196 | while (this._queue.length > this._maxQueueLength) {
197 | this._queue.shift().callback(null, null);
198 | }
199 | };
200 |
201 | /**
202 | * Dispatch a job.
203 | */
204 | Processor.prototype._dispatch = function() {
205 | if (this._running === 0 && this._queue.length > 0) {
206 | const job = (this._job = this._queue.shift());
207 | const width = job.inputs[0].width;
208 | const height = job.inputs[0].height;
209 | const buffers = job.inputs.map(function(input) {
210 | return input.data.buffer;
211 | });
212 | const threads = this._workers.length;
213 | this._running = threads;
214 | if (threads === 1) {
215 | this._workers[0].postMessage(
216 | {
217 | buffers: buffers,
218 | meta: job.meta,
219 | imageOps: this._imageOps,
220 | width: width,
221 | height: height
222 | },
223 | buffers
224 | );
225 | } else {
226 | const length = job.inputs[0].data.length;
227 | const segmentLength = 4 * Math.ceil(length / 4 / threads);
228 | for (let i = 0; i < threads; ++i) {
229 | const offset = i * segmentLength;
230 | const slices = [];
231 | for (let j = 0, jj = buffers.length; j < jj; ++j) {
232 | slices.push(buffers[i].slice(offset, offset + segmentLength));
233 | }
234 | this._workers[i].postMessage(
235 | {
236 | buffers: slices,
237 | meta: job.meta,
238 | imageOps: this._imageOps,
239 | width: width,
240 | height: height
241 | },
242 | slices
243 | );
244 | }
245 | }
246 | }
247 | };
248 |
249 | /**
250 | * Handle messages from the worker.
251 | * @param {number} index The worker index.
252 | * @param {MessageEvent} event The message event.
253 | */
254 | Processor.prototype._onWorkerMessage = function(index, event) {
255 | if (this._destroyed) {
256 | return;
257 | }
258 | this._dataLookup[index] = event.data;
259 | --this._running;
260 | if (this._running === 0) {
261 | this._resolveJob();
262 | }
263 | };
264 |
265 | /**
266 | * Resolve a job. If there are no more worker threads, the processor callback
267 | * will be called.
268 | */
269 | Processor.prototype._resolveJob = function() {
270 | const job = this._job;
271 | const threads = this._workers.length;
272 | let data, meta;
273 | if (threads === 1) {
274 | data = new Uint8ClampedArray(this._dataLookup[0]['buffer']);
275 | meta = this._dataLookup[0]['meta'];
276 | } else {
277 | const length = job.inputs[0].data.length;
278 | data = new Uint8ClampedArray(length);
279 | meta = new Array(length);
280 | const segmentLength = 4 * Math.ceil(length / 4 / threads);
281 | for (let i = 0; i < threads; ++i) {
282 | const buffer = this._dataLookup[i]['buffer'];
283 | const offset = i * segmentLength;
284 | data.set(new Uint8ClampedArray(buffer), offset);
285 | meta[i] = this._dataLookup[i]['meta'];
286 | }
287 | }
288 | this._job = null;
289 | this._dataLookup = {};
290 | job.callback(
291 | null,
292 | newImageData(data, job.inputs[0].width, job.inputs[0].height),
293 | meta
294 | );
295 | this._dispatch();
296 | };
297 |
298 | module.exports = Processor;
299 |
--------------------------------------------------------------------------------
/lib/util.js:
--------------------------------------------------------------------------------
1 | let hasImageData = true;
2 | try {
3 | new ImageData(10, 10);
4 | } catch (_) {
5 | hasImageData = false;
6 | }
7 |
8 | let context;
9 |
10 | function newImageData(data, width, height) {
11 | if (hasImageData) {
12 | return new ImageData(data, width, height);
13 | }
14 |
15 | if (!context) {
16 | context = document.createElement('canvas').getContext('2d');
17 | }
18 | const imageData = context.createImageData(width, height);
19 | imageData.data.set(data);
20 | return imageData;
21 | }
22 |
23 | exports.newImageData = newImageData;
24 |
--------------------------------------------------------------------------------
/license.md:
--------------------------------------------------------------------------------
1 | # License for pixelworks
2 | The pixelworks module is distributed under the MIT license. Find the full source here: http://tschaub.mit-license.org/
3 |
4 | Copyright Tim Schaub.
5 |
6 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
7 |
8 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
9 |
10 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "pixelworks",
3 | "version": "1.1.0",
4 | "description": "Utilities for working with ImageData in Workers",
5 | "keywords": [
6 | "ImageData",
7 | "worker",
8 | "workers",
9 | "canvas",
10 | "pixels"
11 | ],
12 | "main": "lib/index.js",
13 | "scripts": {
14 | "lint": "eslint lib test examples",
15 | "pretest": "npm run lint",
16 | "test": "karma start test/karma.conf.js --single-run",
17 | "start": "karma start test/karma.conf.js",
18 | "build": "esbuild lib/index.js --bundle --minify --sourcemap --format=iife --global-name=pixelworks --outfile=dist/pixelworks.js"
19 | },
20 | "repository": {
21 | "type": "git",
22 | "url": "git://github.com/tschaub/pixelworks.git"
23 | },
24 | "bugs": {
25 | "url": "https://github.com/tschaub/pixelworks/issues"
26 | },
27 | "license": "MIT",
28 | "devDependencies": {
29 | "chai": "^4.3.4",
30 | "chai-spies": "^1.0.0",
31 | "esbuild": "^0.12.15",
32 | "eslint": "^7.30.0",
33 | "eslint-config-tschaub": "^13.1.0",
34 | "karma": "^6.3.4",
35 | "karma-chrome-launcher": "^3.1.0",
36 | "karma-esbuild": "^2.2.0",
37 | "karma-mocha": "^2.0.1",
38 | "mocha": "^9.0.2"
39 | },
40 | "eslintConfig": {
41 | "extends": "tschaub",
42 | "globals": {
43 | "ImageData": false
44 | },
45 | "rules": {
46 | "dot-notation": 0
47 | }
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/readme.md:
--------------------------------------------------------------------------------
1 | # **pixelworks**
2 |
3 | Utilities for working with [`ImageData`](https://developer.mozilla.org/en-US/docs/Web/API/ImageData) in [`Workers`](https://developer.mozilla.org/en-US/docs/Web/API/Worker/Worker).
4 |
5 | ## Install
6 |
7 | The `pixelworks` package is meant to be used in a browser with a CommonJS module loader (e.g. [Browserify](http://browserify.org/) or [Webpack](http://webpack.github.io/)). Add it as a dependency to your project with `npm`:
8 |
9 | npm install pixelworks
10 |
11 | ## Use
12 |
13 | var pixelworks = require('pixelworks');
14 |
15 | ## API
16 |
17 | ### `new Processor(options)`
18 |
19 | A processor runs pixel or image operations in workers.
20 |
21 | var processor = new pixelworks.Processor(options);
22 |
23 | #### Supported options
24 |
25 | * `imageOps : boolean` - By default, operations will be called for each pixel. By setting `imageOps: true`, operations will be called with an `ImageData` object.
26 |
27 | * `operation : Function` - A function that processes input data and returns output data. The operation will be called with two arguments: an array of inputs, and a user storage object. By default, operations will be called for each pixel in the input data, and the first argument is an array of input pixels (each a `[R, G, B, A]` array). If `imageOps` is `true`, the first argument will be an array of `ImageData` objects. The second object is the user storage object passed to the `process` method.
28 |
29 | Operations return processed output data. For pixel-wise operations, this must be an output pixel (a `[R, G, B, A]` array). For image operations, this must be an `ImageData` object.
30 |
31 | Because operations run in workers, they must only operate on the arguments they are given.
32 |
33 | * `lib : Object` - An optional lookup of functions that can be accessed by an operation run in a worker. Because operations are run in workers, they cannot access functions from the scope where they are authored. The `lib` object can be used to pass additional library functions that are made available in the worker scope. For example, if `{lib: {someFunc: function() {/* do something */}}}` were provided, the operation could call `someFunc()`.
34 |
35 | * `threads : number` - Pixel-wise operations can be run in parallel in multiple worker threads. By default, a single worker thread is created for running operations. Setting `threads: 2` would process half of the input pixels in one thread and half in another. For image type operations, `threads` cannot be greater than `1`. If you want to force operations to run in the main (UI) thread, set `threads: 0`.
36 |
37 | * `queue : number` - Maximum queue length. This limits the number of pending workers when `process` is called multiple times before work completes. If you want to call `process` many times (in response to user generated events for example), set `queue: 1`, and only one worker will be pending at a time.
38 |
39 | ### `processor.process(inputs, meta, callback)`
40 |
41 | Run the operation on an array of input image data.
42 |
43 | * `inputs : Array.` - Array of pixels or image data (depending on the operation type).
44 | * `meta : Object` - A user data object. This is passed to all operations and must be serializable.
45 | * `callback : function(Error, ImageData, Object)` - Called when work completes. The first argument is any error. The second is the `ImageData` generated by the operation. The third is the user data object. When `process` is called repeatedly, a queue of pending workers will be generated. If this queue exceeds the maximum `queue` length, workers will be removed from the queue and the callback will be called with `null` for the second `ImageData` argument.
46 |
47 | ### `processor.destroy()`
48 |
49 | Stop responding to any completed work and destroy the processor.
50 |
51 |
52 | [](https://travis-ci.org/tschaub/pixelworks)
53 |
--------------------------------------------------------------------------------
/test/karma.conf.js:
--------------------------------------------------------------------------------
1 | module.exports = function(config) {
2 | config.set({
3 | browsers: [process.env.CI ? 'ChromeHeadless' : 'Chrome'],
4 | frameworks: ['mocha'],
5 | preprocessors: {
6 | '**/*.test.js': ['esbuild']
7 | },
8 | files: ['**/*.test.js']
9 | });
10 | };
11 |
--------------------------------------------------------------------------------
/test/lib/processor.test.js:
--------------------------------------------------------------------------------
1 | /* eslint-env mocha */
2 |
3 | const Processor = require('../../lib/processor');
4 | const chai = require('chai');
5 | const newImageData = require('../../lib/util').newImageData;
6 | const spies = require('chai-spies');
7 |
8 | chai.use(spies);
9 | const assert = chai.assert;
10 |
11 | describe('Processor', function() {
12 | const identity = function(inputs) {
13 | return inputs[0];
14 | };
15 |
16 | describe('constructor', function() {
17 | it('creates a new processor', function() {
18 | const processor = new Processor({
19 | operation: identity
20 | });
21 |
22 | assert.instanceOf(processor, Processor);
23 | });
24 | });
25 |
26 | describe('#process()', function() {
27 | it('calls operation with input pixels', function(done) {
28 | const processor = new Processor({
29 | operation: function(inputs, meta) {
30 | ++meta.count;
31 | const pixel = inputs[0];
32 | for (let i = 0, ii = pixel.length; i < ii; ++i) {
33 | meta.sum += pixel[i];
34 | }
35 | return pixel;
36 | }
37 | });
38 |
39 | const array = new Uint8ClampedArray([1, 2, 3, 4, 5, 6, 7, 8]);
40 | const input = newImageData(array, 1, 2);
41 |
42 | processor.process([input], {count: 0, sum: 0}, function(err, output, m) {
43 | if (err) {
44 | done(err);
45 | return;
46 | }
47 | assert.equal(m.count, 2);
48 | assert.equal(m.sum, 36);
49 | done();
50 | });
51 | });
52 |
53 | it('calls callback with processed image data', function(done) {
54 | const processor = new Processor({
55 | operation: function(inputs) {
56 | const pixel = inputs[0];
57 | pixel[0] *= 2;
58 | pixel[1] *= 2;
59 | pixel[2] *= 2;
60 | pixel[3] *= 2;
61 | return pixel;
62 | }
63 | });
64 |
65 | const array = new Uint8ClampedArray([1, 2, 3, 4, 5, 6, 7, 8]);
66 | const input = newImageData(array, 1, 2);
67 |
68 | processor.process([input], {}, function(err, output, m) {
69 | if (err) {
70 | done(err);
71 | return;
72 | }
73 | assert.instanceOf(output, ImageData);
74 | assert.deepEqual(
75 | output.data,
76 | new Uint8ClampedArray([2, 4, 6, 8, 10, 12, 14, 16])
77 | );
78 | done();
79 | });
80 | });
81 |
82 | it('allows library functions to be called', function(done) {
83 | const lib = {
84 | sum: function(a, b) {
85 | return a + b;
86 | },
87 | diff: function(a, b) {
88 | return a - b;
89 | }
90 | };
91 |
92 | const normalizedDiff = function(pixels) {
93 | const pixel = pixels[0];
94 | const r = pixel[0];
95 | const g = pixel[1];
96 | const nd = diff(r, g) / sum(r, g); // eslint-disable-line no-undef
97 | const index = Math.round((255 * (nd + 1)) / 2);
98 | return [index, index, index, pixel[3]];
99 | };
100 |
101 | const processor = new Processor({
102 | operation: normalizedDiff,
103 | lib: lib
104 | });
105 |
106 | const array = new Uint8ClampedArray([10, 2, 0, 0, 5, 8, 0, 1]);
107 | const input = newImageData(array, 1, 2);
108 |
109 | processor.process([input], {}, function(err, output, m) {
110 | if (err) {
111 | done(err);
112 | return;
113 | }
114 | assert.instanceOf(output, ImageData);
115 | const v0 = Math.round((255 * (1 + 8 / 12)) / 2);
116 | const v1 = Math.round((255 * (1 + -3 / 13)) / 2);
117 | assert.deepEqual(
118 | output.data,
119 | new Uint8ClampedArray([v0, v0, v0, 0, v1, v1, v1, 1])
120 | );
121 |
122 | done();
123 | });
124 | });
125 |
126 | it('calls callbacks for each call', function(done) {
127 | const processor = new Processor({
128 | operation: identity
129 | });
130 |
131 | let calls = 0;
132 |
133 | function createCallback(index) {
134 | return function(err, output, meta) {
135 | if (err) {
136 | done(err);
137 | return;
138 | }
139 | assert.instanceOf(output, ImageData);
140 | ++calls;
141 | };
142 | }
143 |
144 | for (let i = 0; i < 5; ++i) {
145 | const input = newImageData(new Uint8ClampedArray([1, 2, 3, 4]), 1, 1);
146 | processor.process([input], {}, createCallback(i));
147 | }
148 |
149 | setTimeout(function() {
150 | assert.equal(calls, 5);
151 | done();
152 | }, 1000);
153 | });
154 |
155 | it('respects max queue length', function(done) {
156 | const processor = new Processor({
157 | queue: 1,
158 | operation: identity
159 | });
160 |
161 | const log = [];
162 |
163 | function createCallback(index) {
164 | return function(err, output, meta) {
165 | if (err) {
166 | done(err);
167 | return;
168 | }
169 | log.push(output);
170 | };
171 | }
172 |
173 | for (let i = 0; i < 5; ++i) {
174 | const input = newImageData(new Uint8ClampedArray([1, 2, 3, 4]), 1, 1);
175 | processor.process([input], {}, createCallback(i));
176 | }
177 |
178 | setTimeout(function() {
179 | assert.lengthOf(log, 5);
180 | assert.isNull(log[0], 'first call null');
181 | assert.isNull(log[1], 'second call null');
182 | assert.isNull(log[2], 'third call null');
183 | assert.instanceOf(log[3], ImageData);
184 | assert.instanceOf(log[4], ImageData);
185 | done();
186 | }, 1000);
187 | });
188 | });
189 |
190 | describe('#process() - faux worker', function() {
191 | let identitySpy;
192 | beforeEach(function() {
193 | identitySpy = chai.spy(identity);
194 | });
195 |
196 | it('calls operation with input pixels', function(done) {
197 | const processor = new Processor({
198 | threads: 0,
199 | operation: identitySpy
200 | });
201 |
202 | const array = new Uint8ClampedArray([1, 2, 3, 4, 5, 6, 7, 8]);
203 | const input = newImageData(array, 1, 2);
204 |
205 | processor.process([input], {}, function(err, output, m) {
206 | if (err) {
207 | done(err);
208 | return;
209 | }
210 | assert.lengthOf(identitySpy.__spy.calls, 2);
211 | const first = identitySpy.__spy.calls[0];
212 | assert.lengthOf(first, 2);
213 | done();
214 | });
215 | });
216 |
217 | it('passes meta object to operations', function(done) {
218 | const processor = new Processor({
219 | threads: 0,
220 | operation: identitySpy
221 | });
222 |
223 | const array = new Uint8ClampedArray([1, 2, 3, 4]);
224 | const input = newImageData(array, 1, 1);
225 | const meta = {foo: 'bar'};
226 |
227 | processor.process([input], meta, function(err, output, m) {
228 | if (err) {
229 | done(err);
230 | return;
231 | }
232 | assert.deepEqual(m, meta);
233 | assert.lengthOf(identitySpy.__spy.calls, 1);
234 | done();
235 | });
236 | });
237 | });
238 |
239 | describe('#destroy()', function() {
240 | it('stops callbacks from being called', function(done) {
241 | const processor = new Processor({
242 | operation: identity
243 | });
244 |
245 | const array = new Uint8ClampedArray([1, 2, 3, 4, 5, 6, 7, 8]);
246 | const input = newImageData(array, 1, 2);
247 |
248 | processor.process([input], {}, function() {
249 | done(new Error('Expected abort to stop callback from being called'));
250 | });
251 |
252 | processor.destroy();
253 | setTimeout(done, 500);
254 | });
255 | });
256 |
257 | describe('#destroy() - faux worker', function() {
258 | it('stops callbacks from being called', function(done) {
259 | const processor = new Processor({
260 | threads: 0,
261 | operation: identity
262 | });
263 |
264 | const array = new Uint8ClampedArray([1, 2, 3, 4, 5, 6, 7, 8]);
265 | const input = newImageData(array, 1, 2);
266 |
267 | processor.process([input], {}, function() {
268 | done(new Error('Expected abort to stop callback from being called'));
269 | });
270 |
271 | processor.destroy();
272 | setTimeout(done, 20);
273 | });
274 | });
275 | });
276 |
--------------------------------------------------------------------------------