Using wasm to accelerate PixelManipulation.js (#1093)

* Add wasm code

* First working model

* Add PixelManipulation web assembly code to browser and node

* Tests corrected for modules

* Corrected test script

* Add wasm bechmarks

* Update Readme

* Applies toggling functionality and refactored PixelManipulation code

* Added documentation and corrected wasm toggling

* change noise reduction module to use wasm code

* Corrected formatting  and removed extra comments

* Add default wasm option and made README changes

* Fixed negative test timings

* combined benchmarks file

* Update benchmark.js

* Removed copies of wasm file and corrected test format

* Update package.json

Co-Authored-By: Jeffrey Warren <jeff@unterbahn.com>

* Added wasm file and removed redundant code

* Removed earlier benchmarks

* move test/core/sequencer/benchmark.js to its own test command, not passing to tape-spec

* Solves memory leaks and blank lines

* Solves memory leaks and blank lines

* Added handler for node code

* Modify test script

* Modify test script

* Correct doc and removed pace fuctionality
This commit is contained in:
Slytherin
2019-06-21 20:24:56 +05:30
committed by Jeffrey Warren
parent c3af98ea93
commit 30659d4656
48 changed files with 2243 additions and 2203 deletions

View File

@@ -280,7 +280,6 @@ module.exports = function ModuleName(options,UI) {
The `progressObj` parameter of `draw()` is not consumed unless a custom progress bar needs to be drawn, for which this default spinner should be stopped with `progressObj.stop()` and image-sequencer is informed about the custom progress bar with `progressObj.overrideFlag = true;` following which this object can be overriden with custom progress object.
The pixelManipulation API can draw progress bars internally using the `pace` npm package. The option is disabled by default but can be enabled by passing `ui: true` in the options for pixelManipulation. The recommended way is to use `ui: options.step.ui`. This will only show the progress if the ui is set to true by the user, while creating the sequencer object.
### Module example

View File

@@ -578,3 +578,24 @@ sequencer2.run();
This method returns an object which defines the name and inputs of the modules. If a module name (hyphenated) is passed in the method, then only the details of that module are returned.
The `notify` function takes two parameters `msg` and `id`, former being the message to be displayed on console (in case of CLI and node ) and a HTML component(in browser). The id is optional and is useful for HTML interface to give appropriate IDs.
## Using WebAssembly for heavy pixel processing
Any module which uses the `changePixel` function gets WebAssembly acceleration (`wasm`). Both node and browser code use WebAssembly and the only code which falls back to non-`wasm` code is the [browserified unit tests](https://github.com/publiclab/image-sequencer/blob/main/test/core/sequencer/benchmark.js).
The main advantage we get using `wasm` is blazing fast speed attained in processing pixels for many modules that is very clear from [checking module benchmarks](https://travis-ci.org/publiclab/image-sequencer/jobs/544415673#L1931).
The only limitation is that browser and node code for `wasm` had to be written separately, and switched between. This is because in browser we use `fetch` to retrieve the compiled `wasm` program while in node we use the `fs` module, each of which cannot be used in the other's environment.
`wasm` mode is enabled by default. If you need to force this mode to be on or off, you can use the `useWasm` option when initializing ImageSequencer:
```js
let sequencer = ImageSequencer({useWasm:true}) // for wasm mode or simply
let sequencer = ImageSequencer() // also for wasm mode i.e. default mode
let sequencer = ImageSequencer({useWasm:false}) //for non-wasm mode
```

BIN
dist/manipulation.wasm vendored Normal file

Binary file not shown.

View File

@@ -81,7 +81,7 @@ function DefaultHtmlStepUi(_sequencer, options) {
var inputDesc = isInput ? mapHtmlTypes(inputs[paramName]) : {};
if (!isInput) {
html += '<span class="output"></span>';
}
}
else if (inputDesc.type.toLowerCase() == 'select') {
html += '<select class="form-control target" name="' + paramName + '">';
@@ -94,7 +94,7 @@ function DefaultHtmlStepUi(_sequencer, options) {
let paramVal = step.options[paramName] || inputDesc.default;
if (inputDesc.id == 'color-picker') { // separate input field for color-picker
html +=
html +=
'<div id="color-picker" class="input-group colorpicker-component">' +
'<input class="form-control target" type="' +
inputDesc.type +
@@ -122,7 +122,7 @@ function DefaultHtmlStepUi(_sequencer, options) {
'"max="' +
inputDesc.max +
'"step="' +
(inputDesc.step ? inputDesc.step : 1)+ '">' + '<span>' + paramVal + '</span>';
(inputDesc.step ? inputDesc.step : 1) + '">' + '<span>' + paramVal + '</span>';
}
else html += '">';

4123
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -5,7 +5,7 @@
"main": "src/ImageSequencer.js",
"scripts": {
"debug": "TEST=true node ./index.js -i ./examples/images/monarch.png -s invert",
"test": "TEST=true istanbul cover tape test/core/*.js test/core/ui/user-interface.js test/core/modules/*.js | tap-spec; browserify test/core/sequencer/image-sequencer.js test/core/sequencer/chain.js test/core/sequencer/meta-modules.js test/core/sequencer/replace.js test/core/sequencer/import-export.js test/core/sequencer/run.js test/core/sequencer/dynamic-imports.js test/core/util/*.js test/core/sequencer/benchmark.js | tape-run --render=\"tap-spec\"",
"test": "TEST=true istanbul cover tape test/core/*.js test/core/ui/user-interface.js test/core/modules/*.js | tap-spec; node test/core/sequencer/benchmark.js; browserify test/core/sequencer/meta-modules.js test/core/sequencer/image-sequencer.js test/core/sequencer/chain.js test/core/sequencer/replace.js test/core/sequencer/import-export.js test/core/sequencer/run.js test/core/sequencer/dynamic-imports.js test/core/util/*.js | tape-run --render=\"tap-spec\"",
"test-ui": "node node_modules/jasmine/bin/jasmine test/spec/*.js",
"setup": "npm i && npm i -g grunt grunt-cli && grunt build",
"start": "grunt serve"

View File

@@ -8,7 +8,6 @@ ImageSequencer = function ImageSequencer(options) {
options = options || {};
options.inBrowser = options.inBrowser === undefined ? isBrowser : options.inBrowser;
options.sequencerCounter = 0;
function objTypeOf(object) {
return Object.prototype.toString.call(object).split(' ')[1].slice(0, -1);
}

View File

@@ -25,7 +25,7 @@ function InsertStep(ref, index, name, o) {
o.selector = o_.selector || 'ismod-' + name;
o.container = o_.container || ref.options.selector;
o.inBrowser = ref.options.inBrowser;
o.useWasm = (ref.options.useWasm === false) ? false : true;
if (index == -1) index = ref.steps.length;
o.step = {

View File

@@ -28,7 +28,6 @@ module.exports = function AddQR(options, UI) {
function output(image, datauri, mimetype) {
step.output = { src: datauri, format: mimetype };
}
return require('../_nomodule/PixelManipulation.js')(input, {
output: output,
ui: options.step.ui,
@@ -37,7 +36,8 @@ module.exports = function AddQR(options, UI) {
format: input.format,
image: options.image,
inBrowser: options.inBrowser,
callback: callback
callback: callback,
useWasm:options.useWasm
});
});

View File

@@ -62,7 +62,8 @@ module.exports = function Average(options, UI) {
extraManipulation: extraManipulation,
format: input.format,
image: options.image,
callback: callback
callback: callback,
useWasm:options.useWasm
});
}

View File

@@ -62,7 +62,8 @@ module.exports = function Dynamic(options, UI, util) {
format: input.format,
image: options.image,
inBrowser: options.inBrowser,
callback: callback
callback: callback,
useWasm:options.useWasm
});
});
}

View File

@@ -33,7 +33,8 @@ module.exports = function Blur(options, UI) {
extraManipulation: extraManipulation,
format: input.format,
image: options.image,
callback: callback
callback: callback,
useWasm:options.useWasm
});
}

View File

@@ -45,7 +45,8 @@ module.exports = function Brightness(options, UI) {
format: input.format,
image: options.image,
inBrowser: options.inBrowser,
callback: callback
callback: callback,
useWasm:options.useWasm
});
}

View File

@@ -50,7 +50,8 @@ module.exports = function canvasResize(options, UI) {
format: input.format,
image: options.image,
inBrowser: options.inBrowser,
callback: callback
callback: callback,
useWasm:options.useWasm
});
}

View File

@@ -36,7 +36,8 @@ module.exports = function Channel(options, UI) {
format: input.format,
image: options.image,
inBrowser: options.inBrowser,
callback: callback
callback: callback,
useWasm:options.useWasm
});
}

View File

@@ -68,7 +68,8 @@ module.exports = function ColorTemperature(options, UI) {
format: input.format,
image: options.image,
inBrowser: options.inBrowser,
callback: callback
callback: callback,
useWasm:options.useWasm
});
}

View File

@@ -29,7 +29,8 @@ module.exports = function Colormap(options, UI) {
format: input.format,
image: options.image,
inBrowser: options.inBrowser,
callback: callback
callback: callback,
useWasm:options.useWasm
});
}

View File

@@ -33,7 +33,8 @@ module.exports = function Contrast(options, UI) {
extraManipulation: extraManipulation,
format: input.format,
image: options.image,
callback: callback
callback: callback,
useWasm:options.useWasm
});
}

View File

@@ -31,7 +31,8 @@ module.exports = function Convolution(options, UI) {
extraManipulation: extraManipulation,
format: input.format,
image: options.image,
callback: callback
callback: callback,
useWasm:options.useWasm
});
}

View File

@@ -40,7 +40,8 @@ module.exports = function DoNothing(options, UI) {
ui: options.step.ui,
format: input.format,
image: options.image,
callback: callback
callback: callback,
useWasm:options.useWasm
});
}

View File

@@ -27,7 +27,8 @@ module.exports = function Dither(options, UI) {
extraManipulation: extraManipulation,
format: input.format,
image: options.image,
callback: callback
callback: callback,
useWasm:options.useWasm
});
}
return {

View File

@@ -32,7 +32,8 @@ module.exports = function DrawRectangle(options, UI) {
extraManipulation: extraManipulation,
format: input.format,
image: options.image,
callback: callback
callback: callback,
useWasm:options.useWasm
});
}

View File

@@ -86,7 +86,8 @@ module.exports = function Dynamic(options, UI) {
format: input.format,
image: options.image,
inBrowser: options.inBrowser,
callback: callback
callback: callback,
useWasm:options.useWasm
});
}

View File

@@ -50,7 +50,8 @@ module.exports = function edgeDetect(options, UI) {
format: input.format,
image: options.image,
inBrowser: options.inBrowser,
callback: callback
callback: callback,
useWasm: options.useWasm
});
});
});

View File

@@ -38,7 +38,8 @@ module.exports = function Exposure(options, UI) {
format: input.format,
image: options.image,
inBrowser: options.inBrowser,
callback: callback
callback: callback,
useWasm:options.useWasm
});
}

View File

@@ -37,7 +37,8 @@ module.exports = function FlipImage(options, UI) {
format: input.format,
image: options.image,
inBrowser: options.inBrowser,
callback: callback
callback: callback,
useWasm:options.useWasm
});
});

View File

@@ -35,7 +35,8 @@ module.exports = function Gamma(options, UI) {
format: input.format,
image: options.image,
inBrowser: options.inBrowser,
callback: callback
callback: callback,
useWasm:options.useWasm
});
}

View File

@@ -29,7 +29,8 @@ module.exports = function GridOverlay(options, UI) {
format: input.format,
image: options.image,
inBrowser: options.inBrowser,
callback: callback
callback: callback,
useWasm: options.useWasm
});

View File

@@ -80,7 +80,8 @@ module.exports = function Channel(options, UI) {
format: input.format,
image: options.image,
inBrowser: options.inBrowser,
callback: callback
callback: callback,
useWasm:options.useWasm
});
}

View File

@@ -32,7 +32,7 @@ module.exports = function ImportImageModule(options, UI) {
step.metadata.input = input;
// options.format = require('../../util/GetFormat')(options.imageUrl);
var helper = ImageSequencer({ inBrowser: options.inBrowser, ui: false });
var helper = ImageSequencer({ inBrowser: options.inBrowser, ui: false, useWasm: options.useWasm });
helper.loadImages(options.imageUrl, () => {
step.output = helper.steps[0].output;
callback();

View File

@@ -30,7 +30,8 @@ function Invert(options, UI) {
format: input.format,
image: options.image,
inBrowser: options.inBrowser,
callback: callback
callback: callback,
useWasm:options.useWasm
});
}

View File

@@ -46,7 +46,8 @@ module.exports = function Ndvi(options, UI) {
format: input.format,
image: options.image,
inBrowser: options.inBrowser,
callback: modifiedCallback
callback: modifiedCallback,
useWasm:options.useWasm
});
}

View File

@@ -27,7 +27,8 @@ module.exports = function NoiseReduction(options, UI) {
extraManipulation: extraManipulation,
format: input.format,
image: options.image,
callback: callback
callback: callback,
useWasm:options.useWasm
});
}
return {

View File

@@ -70,7 +70,8 @@ module.exports = function Dynamic(options, UI, util) {
format: baseStepOutput.format,
image: baseStepImage,
inBrowser: options.inBrowser,
callback: callback
callback: callback,
useWasm:options.useWasm
});
});
}

View File

@@ -28,7 +28,8 @@ module.exports = function PaintBucket(options, UI) {
format: input.format,
image: options.image,
inBrowser: options.inBrowser,
callback: callback
callback: callback,
useWasm:options.useWasm
});
}

View File

@@ -32,7 +32,8 @@ module.exports = function ReplaceColor(options, UI) {
extraManipulation: extraManipulation,
format: input.format,
image: options.image,
callback: callback
callback: callback,
useWasm:options.useWasm
});
}

View File

@@ -3,7 +3,7 @@ module.exports = exports = function(pixels, options){
color = color.substring(color.indexOf('(') + 1, color.length - 1); // extract only the values from rgba(_,_,_,_)
var replaceColor = options.replaceColor || 'rgb(0,0,255)';
replaceColor = replaceColor.substring(replaceColor.indexOf('(') + 1 , replaceColor.length - 1); // extract only the values from rgba(_,_,_,_)
replaceColor = replaceColor.substring(replaceColor.indexOf('(') + 1, replaceColor.length - 1); // extract only the values from rgba(_,_,_,_)
var replaceMethod = options.replaceMethod || 'greyscale';
color = color.split(',');

View File

@@ -60,7 +60,8 @@ module.exports = function Resize(options, UI) {
format: input.format,
image: options.image,
inBrowser: options.inBrowser,
callback: callback
callback: callback,
useWasm:options.useWasm
});
}

View File

@@ -51,7 +51,8 @@ module.exports = function Rotate(options, UI) {
format: input.format,
image: options.image,
inBrowser: options.inBrowser,
callback: callback
callback: callback,
useWasm:options.useWasm
});
}

View File

@@ -45,7 +45,8 @@ module.exports = function Saturation(options, UI) {
format: input.format,
image: options.image,
inBrowser: options.inBrowser,
callback: callback
callback: callback,
useWasm:options.useWasm
});
}

View File

@@ -36,7 +36,8 @@ module.exports = function TextOverlay(options, UI) {
format: input.format,
image: options.image,
inBrowser: options.inBrowser,
callback: callback
callback: callback,
useWasm:options.useWasm
});
}

View File

@@ -33,7 +33,8 @@ module.exports = function ImageThreshold(options, UI) {
extraManipulation: extraManipulation,
format: input.format,
image: options.image,
callback: callback
callback: callback,
useWasm:options.useWasm
});
}
return {

View File

@@ -39,7 +39,8 @@ module.exports = function Tint(options, UI) {
format: input.format,
image: options.image,
inBrowser: options.inBrowser,
callback: callback
callback: callback,
useWasm:options.useWasm
});
}

View File

@@ -45,7 +45,8 @@ module.exports = function Balance(options, UI) {
format: input.format,
image: options.image,
inBrowser: options.inBrowser,
callback: callback
callback: callback,
useWasm:options.useWasm
});
}

View File

@@ -1,9 +1,8 @@
/*
* General purpose per-pixel manipulation
* accepting a changePixel() method to remix a pixel's channels
*/
* General purpose per-pixel manipulation
* accepting a changePixel() method to remix a pixel's channels
*/
module.exports = function PixelManipulation(image, options) {
// To handle the case where pixelmanipulation is called on the input object itself
// like input.pixelManipulation(options)
if (arguments.length <= 1) {
@@ -16,7 +15,7 @@ module.exports = function PixelManipulation(image, options) {
const getPixels = require('get-pixels'),
savePixels = require('save-pixels');
getPixels(image.src, function(err, pixels) {
getPixels(image.src, function (err, pixels) {
if (err) {
console.log('Bad image path', image);
return;
@@ -32,69 +31,117 @@ module.exports = function PixelManipulation(image, options) {
// TODO: this could possibly be more efficient; see
// https://github.com/p-v-o-s/infragram-js/blob/master/public/infragram.js#L173-L181
if (!options.inBrowser && !process.env.TEST && options.ui) {
try {
var pace = require('pace')(pixels.shape[0] * pixels.shape[1]);
} catch (e) {
options.inBrowser = true;
}
}
if (options.preProcess) pixels = options.preProcess(pixels); // Allow for preprocessing
function extraOperation() {
var res;
if (options.extraManipulation) res = options.extraManipulation(pixels, generateOutput);
// there may be a more efficient means to encode an image object,
// but node modules and their documentation are essentially arcane on this point
function generateOutput() {
var chunks = [];
var totalLength = 0;
var r = savePixels(pixels, options.format, {
quality: 100
});
r.on('data', function (chunk) {
totalLength += chunk.length;
chunks.push(chunk);
});
r.on('end', function () {
var data = Buffer.concat(chunks, totalLength).toString('base64');
var datauri = 'data:image/' + options.format + ';base64,' + data;
if (options.output)
options.output(options.image, datauri, options.format);
if (options.callback) options.callback();
});
}
if (res) {
pixels = res;
generateOutput();
} else if (!options.extraManipulation) generateOutput();
}
if (!options.changePixel) extraOperation();
if (options.changePixel) {
/* Allows for Flexibility
if per pixel manipulation is not required */
for (var x = 0; x < pixels.shape[0]; x++) {
for (var y = 0; y < pixels.shape[1]; y++) {
let pixel = options.changePixel(
pixels.get(x, y, 0),
pixels.get(x, y, 1),
pixels.get(x, y, 2),
pixels.get(x, y, 3),
x,
y
);
const imports = {
env: {
consoleLog: console.log,
perform: function (x, y) {
let pixel = options.changePixel(
pixels.get(x, y, 0),
pixels.get(x, y, 1),
pixels.get(x, y, 2),
pixels.get(x, y, 3),
x,
y
);
pixels.set(x, y, 0, pixel[0]);
pixels.set(x, y, 1, pixel[1]);
pixels.set(x, y, 2, pixel[2]);
pixels.set(x, y, 3, pixel[3]);
pixels.set(x, y, 0, pixel[0]);
pixels.set(x, y, 1, pixel[1]);
pixels.set(x, y, 2, pixel[2]);
pixels.set(x, y, 3, pixel[3]);
}
}
};
if (!options.inBrowser && !process.env.TEST && options.ui) pace.op();
function perPixelManipulation() { // pure JavaScript code
for (var x = 0; x < pixels.shape[0]; x++) {
for (var y = 0; y < pixels.shape[1]; y++) {
imports.env.perform(x, y);
}
}
}
}
// perform any extra operations on the entire array:
var res;
if (options.extraManipulation) res = options.extraManipulation(pixels, generateOutput);
// there may be a more efficient means to encode an image object,
// but node modules and their documentation are essentially arcane on this point
function generateOutput() {
var chunks = [];
var totalLength = 0;
var r = savePixels(pixels, options.format, { quality: 100 });
const inBrowser = (options.inBrowser) ? 1 : 0;
const test = (process.env.TEST) ? 1 : 0;
if (options.useWasm) {
if (options.inBrowser) {
r.on('data', function(chunk) {
totalLength += chunk.length;
chunks.push(chunk);
});
r.on('end', function() {
var data = Buffer.concat(chunks, totalLength).toString('base64');
var datauri = 'data:image/' + options.format + ';base64,' + data;
if (options.output)
options.output(options.image, datauri, options.format);
if (options.callback) options.callback();
});
fetch('../../../dist/manipulation.wasm').then(response =>
response.arrayBuffer()
).then(bytes =>
WebAssembly.instantiate(bytes, imports)
).then(results => {
results.instance.exports.manipulatePixel(pixels.shape[0], pixels.shape[1], inBrowser, test);
extraOperation();
}).catch(err => {
console.log(err);
console.log('WebAssembly acceleration errored; falling back to JavaScript in PixelManipulation');
perPixelManipulation();
extraOperation();
});
} else {
try{
const fs = require('fs');
const path = require('path');
const wasmPath = path.join(__dirname, '../../../', 'dist', 'manipulation.wasm');
const buf = fs.readFileSync(wasmPath);
WebAssembly.instantiate(buf, imports).then(results => {
results.instance.exports.manipulatePixel(pixels.shape[0], pixels.shape[1], inBrowser, test);
extraOperation();
});
}
catch(err){
console.log(err);
console.log('WebAssembly acceleration errored; falling back to JavaScript in PixelManipulation');
perPixelManipulation();
extraOperation();
}
}
} else {
perPixelManipulation();
extraOperation();
}
}
if (res) {
pixels = res;
generateOutput();
}
else if (!options.extraManipulation) generateOutput();
});
};
};

View File

@@ -3,6 +3,7 @@
var fs = require('fs');
var test = require('tape');
var DataURItoBuffer = require('data-uri-to-buffer');
require('events').EventEmitter.prototype.setMaxListeners(100);
ImageSequencer = require('../../../src/ImageSequencer.js');
@@ -11,10 +12,23 @@ var image = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAQAAAAEACAIAAADTED8xA
var imageName = 'image1';
test('benchmark all modules', function(t) {
var sequencerDefault = ImageSequencer({ ui: false, inBrowser: false, useWasm:false });
console.log('############ Benchmarks ############');
runBenchmarks(sequencerDefault, t);
console.log('####################################');
var sequencer = ImageSequencer({ ui: false, inBrowser: false });
});
test('benchmark all modules with WebAssembly', function(t) {
var sequencerWebAssembly = ImageSequencer({ ui: false, inBrowser: false, useWasm: true });
console.log('############ Benchmarks with WebAssembly ############');
runBenchmarks(sequencerWebAssembly, t);
console.log('####################################');
});
function runBenchmarks(sequencer, t) {
var mods = Object.keys(sequencer.modules);
sequencer.loadImages(image);
@@ -29,11 +43,11 @@ test('benchmark all modules', function(t) {
var end = Date.now();
console.log('Module ' + mods[0] + ' ran in: ' + (end - global.start) + ' milliseconds');
mods.splice(0, 1);
if (mods.length > 1) { //Last one is test module, we need not benchmark it
if (mods.length > 1) { // Last one is test module, we need not benchmark it
sequencer.steps[global.idx].output.src = image;
global.idx++;
if (mods[0] === 'import-image' || (!!sequencer.modulesInfo(mods[0]).requires && sequencer.modulesInfo(mods[0]).requires.includes('webgl'))) {
/* Not currently working */
/* Not currently working for this module, which is for importing a new image */
console.log('Bypassing import-image');
mods.splice(0, 1);
}
@@ -41,8 +55,7 @@ test('benchmark all modules', function(t) {
global.start = Date.now();
sequencer.run({ index: global.idx }, cb);
} else {
console.log('####################################');
t.end();
}
}
});
}

View File

@@ -1,7 +1,7 @@
var test = require('tape');
require('../../../src/ImageSequencer.js');
var sequencer = ImageSequencer({ ui: false });
var sequencer = ImageSequencer({ ui: false});
var red = 'data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAYEBQYFBAYGBQYHBwYIChAKCgkJChQODwwQFxQYGBcUFhYaHSUfGhsjHBYWICwgIyYnKSopGR8tMC0oMCUoKSj/2wBDAQcHBwoIChMKChMoGhYaKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCj/wAARCAAQABADASIAAhEBAxEB/8QAFQABAQAAAAAAAAAAAAAAAAAAAAf/xAAUEAEAAAAAAAAAAAAAAAAAAAAA/8QAFQEBAQAAAAAAAAAAAAAAAAAABgj/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwCdABykX//Z';
test('Dynamically add a test Module', function(t) {

View File

@@ -36,7 +36,6 @@ module.exports = (moduleName, options, benchmark, input) => {
test(`${moduleName} module works correctly`, t => {
sequencer.run({mode: 'test'}, () => {
let result = sequencer.steps[1].output.src;
base64Img.imgSync(result, target, 'result');