[GCI] Experimental GIF Manipulation (#1404)

* wasmSuccess

* modules use wasmSuccess

* modules use wasmSuccess

* add the tooltip

* add GIF support

* fix imageDImensions function

* fix inputCoordinatesParser function

* show correct image dimensions

* show correct image dimensions

* don't allow save as PDF for GIFs

* fix QR module

* fix BlobAnalysis

* fix Blur

* fix Colorbar

* fix Crop

* fix crop defaults

* fix DrawRectangle

* fix EdgeDetect

* fix FlipImage

* fix Gradient

* fix Invert

* fix Overlay

* fix Resize

* fix Rotate

* fix TextOverlay

* fix parse input test

* make GIFs work in nodejs

* sample GIF test

* small change

* cleanup

* cleanup

* cleanup

* small fix

* small change

* handle errors

* proper error handling and fix tests

* cleanup

* try a fix

* try another fix

* fix module benchmarks

* try more fixes

* revert

* try fixing the tests

* fix overlay test

* add the gif tests

* remove unnecessary changes

* fix tests

* whoops

* add some docs

* inBrowser

* fix all module tests

Co-authored-by: Jeffrey Warren <jeff@unterbahn.com>
This commit is contained in:
Harsh Khandeparkar
2020-01-11 22:35:10 +05:30
committed by Jeffrey Warren
parent 61b2d75383
commit ea2069d7f6
50 changed files with 922 additions and 613 deletions

View File

@@ -599,3 +599,17 @@ let sequencer = ImageSequencer() // also for wasm mode i.e. default mode
let sequencer = ImageSequencer({useWasm:false}) //for non-wasm mode let sequencer = ImageSequencer({useWasm:false}) //for non-wasm mode
``` ```
## Experimental GIF processing support
ImageSequencer currently can process GIFs but only for most of the modules. Every frame of the GIF is manipulated sequentially (parallel processing would be preferable in the future).
The final frames are then converted back to a GIF but in the process, the time duration of each frame is lost and defaults to `0.1s`.
Modules that do not work:
1. ColorBar (Will get fixed upon fixing overlay as this is a meta module which uses overlay)
2. FisheyeGL
4. Overlay
5. Text Overlay (Almost fixed)
6. Blend
7. Histogram
8. WebGL Distort

View File

@@ -270,23 +270,26 @@ window.onload = function () {
* @param {string} imageDataURL - The data URL for the image. * @param {string} imageDataURL - The data URL for the image.
*/ */
function savePDF(imageDataURL) { function savePDF(imageDataURL) {
sequencer.getImageDimensions(imageDataURL, function(dimensions) { sequencer.getImageDimensions(imageDataURL, function(dimensions, isGIF) {
// Get the dimensions of the image. if (!isGIF) {
let pageWidth = dimensions.width; // Get the dimensions of the image.
let pageHeight = dimensions.height; let pageWidth = dimensions.width;
let pageHeight = dimensions.height;
// Create a new pdf with the same dimensions as the image. // Create a new pdf with the same dimensions as the image.
const pdf = new jsPDF({ const pdf = new jsPDF({
orientation: pageHeight > pageWidth ? 'portrait' : 'landscape', orientation: pageHeight > pageWidth ? 'portrait' : 'landscape',
unit: 'px', unit: 'px',
format: [pageHeight, pageWidth] format: [pageHeight, pageWidth]
}); });
// Add the image to the pdf with dimensions equal to the internal dimensions of the page. // Add the image to the pdf with dimensions equal to the internal dimensions of the page.
pdf.addImage(imageDataURL, 0, 0, pdf.internal.pageSize.getWidth(), pdf.internal.pageSize.getHeight()); pdf.addImage(imageDataURL, 0, 0, pdf.internal.pageSize.getWidth(), pdf.internal.pageSize.getHeight());
// Save the pdf with the filename specified here: // Save the pdf with the filename specified here:
pdf.save('index.pdf'); pdf.save('index.pdf');
}
else console.log('GIFs cannot be converted to PDF');
}); });
} }

View File

@@ -356,14 +356,14 @@ function DefaultHtmlStepUi(_sequencer, options) {
else $step('.wasm-tooltip').fadeOut(); else $step('.wasm-tooltip').fadeOut();
} }
/** /**
* @description Updates Dimension of the image * @description Updates Dimension of the image
* @param {Object} step - Current Step * @param {Object} step - Current Step
* @returns {void} * @returns {void}
* *
*/ */
function updateDimensions(step){ function updateDimensions(step){
_sequencer.getImageDimensions(step.imgElement.src, function (dim) { _sequencer.getImageDimensions(step.imgElement.src, function (dim, isGIF) {
step.ui.querySelector('.' + step.name).attributes['data-original-title'].value = `<div style="text-align: center"><p>Image Width: ${dim.width}<br>Image Height: ${dim.height}</br></div>`; step.ui.querySelector('.' + step.name).attributes['data-original-title'].value = `<div style="text-align: center"><p>Image Width: ${dim.width}<br>Image Height: ${dim.height}</br>${isGIF ? `Frames: ${dim.frames}` : ''}</div>`;
}); });
} }

18
package-lock.json generated
View File

@@ -10364,7 +10364,7 @@
}, },
"load-json-file": { "load-json-file": {
"version": "1.1.0", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-1.1.0.tgz", "resolved": "http://registry.npmjs.org/load-json-file/-/load-json-file-1.1.0.tgz",
"integrity": "sha1-lWkFcI1YtLq0wiYbBPWfMcmTdMA=", "integrity": "sha1-lWkFcI1YtLq0wiYbBPWfMcmTdMA=",
"requires": { "requires": {
"graceful-fs": "^4.1.2", "graceful-fs": "^4.1.2",
@@ -12466,6 +12466,14 @@
"version": "2.1.2", "version": "2.1.2",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
},
"ws": {
"version": "6.2.1",
"resolved": "https://registry.npmjs.org/ws/-/ws-6.2.1.tgz",
"integrity": "sha512-GIyAXC2cB7LjvpgMt9EKS2ldqr0MTrORaleiOno6TweZ6r3TKtoFQWay/2PceJ3RuBasOHzXNn5Lrw1X0bEjqA==",
"requires": {
"async-limiter": "~1.0.0"
}
} }
} }
}, },
@@ -15849,14 +15857,6 @@
"signal-exit": "^3.0.2" "signal-exit": "^3.0.2"
} }
}, },
"ws": {
"version": "6.2.1",
"resolved": "https://registry.npmjs.org/ws/-/ws-6.2.1.tgz",
"integrity": "sha512-GIyAXC2cB7LjvpgMt9EKS2ldqr0MTrORaleiOno6TweZ6r3TKtoFQWay/2PceJ3RuBasOHzXNn5Lrw1X0bEjqA==",
"requires": {
"async-limiter": "~1.0.0"
}
},
"xhr-write-stream": { "xhr-write-stream": {
"version": "0.1.2", "version": "0.1.2",
"resolved": "https://registry.npmjs.org/xhr-write-stream/-/xhr-write-stream-0.1.2.tgz", "resolved": "https://registry.npmjs.org/xhr-write-stream/-/xhr-write-stream-0.1.2.tgz",

View File

@@ -5,7 +5,7 @@
"main": "src/ImageSequencer.js", "main": "src/ImageSequencer.js",
"scripts": { "scripts": {
"debug": "TEST=true node ./index.js -i ./examples/images/monarch.png -s invert", "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; node test/core/sequencer/benchmark.js; grunt tests; cat ./output/core-tests.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 | tap-spec; node test/core/gifs/gif-test.js | tap-spec; grunt tests; cat ./output/core-tests.js | tape-run --render=\"tap-spec\"",
"test-ui": "node node_modules/jasmine/bin/jasmine test/ui/spec/*.js", "test-ui": "node node_modules/jasmine/bin/jasmine test/ui/spec/*.js",
"test-ui-2": "node ./node_modules/.bin/jest", "test-ui-2": "node ./node_modules/.bin/jest",
"setup": "npm i && npm i -g grunt grunt-cli && grunt build $$ npm rebuild --build-from-source", "setup": "npm i && npm i -g grunt grunt-cli && grunt build $$ npm rebuild --build-from-source",

View File

@@ -30,7 +30,8 @@ module.exports = {
'grid-overlay': require('./modules/GridOverlay'), 'grid-overlay': require('./modules/GridOverlay'),
'import-image': require('./modules/ImportImage'), 'import-image': require('./modules/ImportImage'),
'minify-image': require('./modules/MinifyImage'), 'minify-image': require('./modules/MinifyImage'),
'invert': require('image-sequencer-invert'), // 'invert': require('image-sequencer-invert'),
'invert': require('./modules/Invert'),
'ndvi': require('./modules/Ndvi'), 'ndvi': require('./modules/Ndvi'),
'ndvi-colormap': require('./modules/NdviColormap'), 'ndvi-colormap': require('./modules/NdviColormap'),
'noise-reduction': require('./modules/NoiseReduction'), 'noise-reduction': require('./modules/NoiseReduction'),

View File

@@ -1,3 +1,4 @@
const _ = require('lodash');
module.exports = function AddQR(options, UI) { module.exports = function AddQR(options, UI) {
var defaults = require('./../../util/getDefaults.js')(require('./info.json')); var defaults = require('./../../util/getDefaults.js')(require('./info.json'));
@@ -13,34 +14,34 @@ module.exports = function AddQR(options, UI) {
var step = this; var step = this;
return getPixels(input.src, function(err, oldPixels) { function changePixel(r, g, b, a) {
function changePixel(r, g, b, a) { return [r, g, b, a];
return [r, g, b, a]; }
}
function extraManipulation(pixels, generateOutput) { function extraManipulation(pixels, setRenderState, generateOutput) {
if (err) { const oldPixels = _.cloneDeep(pixels);
console.log(err); setRenderState(false); // Prevent rendering of final output image until extraManipulation completes.
return;
}
require('./QR')(options, pixels, oldPixels, generateOutput);
}
function output(image, datauri, mimetype, wasmSuccess) { require('./QR')(options, pixels, oldPixels, () => {
step.output = { src: datauri, format: mimetype, wasmSuccess, useWasm: options.useWasm }; setRenderState(true); // Allow rendering in the callback.
} generateOutput();
return require('../_nomodule/PixelManipulation.js')(input, {
output: output,
ui: options.step.ui,
changePixel: changePixel,
extraManipulation: extraManipulation,
format: input.format,
image: options.image,
inBrowser: options.inBrowser,
callback: callback,
useWasm:options.useWasm
}); });
}
function output(image, datauri, mimetype, wasmSuccess) {
step.output = { src: datauri, format: mimetype, wasmSuccess, useWasm: options.useWasm };
}
return require('../_nomodule/PixelManipulation.js')(input, {
output: output,
ui: options.step.ui,
changePixel: changePixel,
extraManipulation: extraManipulation,
format: input.format,
image: options.image,
inBrowser: options.inBrowser,
callback: callback,
useWasm:options.useWasm
}); });
} }

View File

@@ -1,44 +1,37 @@
module.exports = exports = function (options, pixels, oldPixels, callback) { const pixelSetter = require('../../util/pixelSetter.js'),
const pixelSetter = require('../../util/pixelSetter.js'); getPixels = require('get-pixels'),
QRCode = require('qrcode');
module.exports = exports = function (options, pixels, oldPixels, cb) {
var QRCode = require('qrcode'); QRCode.toDataURL(options.qrCodeString, {width: options.size, scale: 1}, function (error, url) {
QRCode.toDataURL(options.qrCodeString, function (err, url) {
var getPixels = require('get-pixels');
getPixels(url, function (err, qrPixels) { getPixels(url, function (err, qrPixels) {
if (err) { if (err) {
console.log('Bad image path', image); console.log('get-pixels error: ', err);
} }
var imagejs = require('imagejs'); const width = oldPixels.shape[0],
var bitmap = new imagejs.Bitmap({ width: qrPixels.shape[0], height: qrPixels.shape[1] });
bitmap._data.data = qrPixels.data;
var resized = bitmap.resize({
width: options.size, height: options.size,
algorithm: 'bicubicInterpolation'
});
qrPixels.data = resized._data.data;
qrPixels.shape = [options.size, options.size, 4];
qrPixels.stride[1] = 4 * options.size;
var width = oldPixels.shape[0],
height = oldPixels.shape[1]; height = oldPixels.shape[1];
var xe = width - options.size,
const xe = width - options.size, // Starting pixel coordinates
ye = height - options.size; ye = height - options.size;
for (var m = 0; m < width; m++) {
for (var n = 0; n < height; n++) { for (let x = xe; x < width; x++) {
if (m >= xe && n >= ye) { for (let y = ye; y < height; y++) {
pixelSetter(m, n, [qrPixels.get(m - xe, n - ye, 0), qrPixels.get(m - xe, n - ye, 1), qrPixels.get(m - xe, n - ye, 2), qrPixels.get(m - xe, n - ye, 3)], pixels); pixelSetter(
} x,
y,
else { [
pixelSetter(m, n, [oldPixels.get(m, n, 0), oldPixels.get(m, n, 1), oldPixels.get(m, n, 2), oldPixels.get(m, n, 3)], pixels); qrPixels.get(x - xe, y - ye, 0),
} qrPixels.get(x - xe, y - ye, 1),
qrPixels.get(x - xe, y - ye, 2),
qrPixels.get(x - xe, y - ye, 3)
],
pixels
);
} }
} }
callback();
if(cb) cb();
}); });
}); });
}; };

View File

@@ -54,6 +54,7 @@ module.exports = function Average(options, UI) {
return require('../_nomodule/PixelManipulation.js')(input, { return require('../_nomodule/PixelManipulation.js')(input, {
output: output, output: output,
ui: options.step.ui, ui: options.step.ui,
inBrowser: options.inBrowser,
extraManipulation: extraManipulation, extraManipulation: extraManipulation,
format: input.format, format: input.format,
image: options.image, image: options.image,

View File

@@ -1,17 +1,10 @@
module.exports = function(pixels, options, priorStep){ module.exports = function(pixels){
var $ = require('jquery'); // To make Blob-analysis work in Node
var img = $(priorStep.imgElement);
if(Object.keys(img).length === 0){
img = $(priorStep.options.step.imgElement);
}
var canvas = document.createElement('canvas'); var canvas = document.createElement('canvas');
canvas.width = pixels.shape[0]; canvas.width = pixels.shape[0];
canvas.height = pixels.shape[1]; canvas.height = pixels.shape[1];
var ctx = canvas.getContext('2d'); var ctx = canvas.getContext('2d');
ctx.drawImage(img[0], 0, 0); ctx.putImageData(new ImageData(new Uint8ClampedArray(pixels.data), pixels.shape[0], pixels.shape[1]), 0, 0);
let imgData = ctx.getImageData(0, 0, canvas.width, canvas.height); let imgData = ctx.getImageData(0, 0, canvas.width, canvas.height);

View File

@@ -10,11 +10,9 @@ module.exports = function BlobAnalysis(options, UI){
var step = this; var step = this;
var priorStep = this.getStep(-1); // Get the previous step to process it
function extraManipulation(pixels){ function extraManipulation(pixels){
pixels = require('./BlobAnalysis')(pixels, options, priorStep); pixels = require('./BlobAnalysis')(pixels);
return pixels; return pixels;
} }

View File

@@ -1,3 +1,31 @@
// Generates a 5x5 Gaussian kernel
function kernelGenerator(sigma = 1) {
let kernel = [],
sum = 0;
if (sigma == 0) sigma += 0.05;
const s = 2 * Math.pow(sigma, 2);
for (let y = -2; y <= 2; y++) {
kernel.push([]);
for (let x = -2; x <= 2; x++) {
let r = Math.sqrt(Math.pow(x, 2) + Math.pow(y, 2));
kernel[y + 2].push(Math.exp(-(r / s)));
sum += kernel[y + 2][x + 2];
}
}
for (let x = 0; x < 5; x++){
for (let y = 0; y < 5; y++){
kernel[y][x] = (kernel[y][x] / sum);
}
}
return kernel;
}
module.exports = exports = function(pixels, blur) { module.exports = exports = function(pixels, blur) {
const pixelSetter = require('../../util/pixelSetter.js'); const pixelSetter = require('../../util/pixelSetter.js');
@@ -34,32 +62,4 @@ module.exports = exports = function(pixels, blur) {
} }
return pixels; return pixels;
// Generates a 5x5 Gaussian kernel.
function kernelGenerator(sigma = 1) {
let kernel = [],
sum = 0;
if (sigma == 0) sigma += 0.05;
const s = 2 * Math.pow(sigma, 2);
for (let y = -2; y <= 2; y++) {
kernel.push([]);
for (let x = -2; x <= 2; x++) {
let r = Math.sqrt(Math.pow(x, 2) + Math.pow(y, 2));
kernel[y + 2].push(Math.exp(-(r / s)));
sum += kernel[y + 2][x + 2];
}
}
for (let x = 0; x < 5; x++){
for (let y = 0; y < 5; y++){
kernel[y][x] = (kernel[y][x] / sum);
}
}
return kernel;
}
}; };

View File

@@ -27,6 +27,7 @@ module.exports = function Blur(options, UI) {
return require('../_nomodule/PixelManipulation.js')(input, { return require('../_nomodule/PixelManipulation.js')(input, {
output: output, output: output,
ui: options.step.ui, ui: options.step.ui,
inBrowser: options.inBrowser,
extraManipulation: extraManipulation, extraManipulation: extraManipulation,
format: input.format, format: input.format,
image: options.image, image: options.image,

View File

@@ -5,8 +5,8 @@ module.exports = require('../../util/createMetaModule.js')(
return [ return [
{ 'name': 'gradient', 'options': {} }, { 'name': 'gradient', 'options': {} },
{ 'name': 'colormap', 'options': { colormap: options.colormap || defaults.colormap } }, { 'name': 'colormap', 'options': { colormap: options.colormap || defaults.colormap } },
{ 'name': 'crop', 'options': { 'y': 0, 'h': options.h || defaults.h } }, { 'name': 'crop', 'options': { 'y': 0, 'w': '100%', 'h': options.h || defaults.h } },
{ 'name': 'overlay', 'options': { 'x': options.x || defaults.x, 'y': options.y || defaults.y, 'offset': -4 } } { 'name': 'overlay', 'options': { 'x': options.x || defaults.h, 'y': options.y || defaults.y, 'offset': -4 } }
]; ];
}, { }, {
infoJson: require('./info.json') infoJson: require('./info.json')

View File

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

View File

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

View File

@@ -1,61 +1,71 @@
module.exports = function Crop(input, options, callback) { const ndarray = require('ndarray'),
pixelSetter = require('../../util/pixelSetter'),
parseCornerCoordinateInputs = require('../../util/ParseInputCoordinates');
module.exports = function Crop(pixels, options, cb) {
var defaults = require('./../../util/getDefaults.js')(require('./info.json')); var defaults = require('./../../util/getDefaults.js')(require('./info.json'));
var getPixels = require('get-pixels'), options.x = options.x || defaults.x;
savePixels = require('save-pixels'); options.y = options.y || defaults.y;
options.x = parseInt(options.x) || defaults.x; options.w = options.w || defaults.w;
options.y = parseInt(options.y) || defaults.y; options.h = options.h || defaults.h;
getPixels(input.src, function(err, pixels){ options.backgroundColor = options.backgroundColor || defaults.backgroundColor;
options.w = parseInt(options.w) || Math.floor(pixels.shape[0]);
options.h = parseInt(options.h) || Math.floor(pixels.shape[1]);
options.backgroundColor = options.backgroundColor || defaults.backgroundColor;
var ox = options.x;
var oy = options.y;
var w = options.w;
var h = options.h;
var iw = pixels.shape[0]; //Width of Original Image
var ih = pixels.shape[1]; //Height of Original Image
var backgroundArray = [];
backgroundColor = options.backgroundColor.substring(options.backgroundColor.indexOf('(') + 1, options.backgroundColor.length - 1); // extract only the values from rgba(_,_,_,_)
backgroundColor = backgroundColor.split(',');
for(var i = 0; i < w ; i++){
backgroundArray = backgroundArray.concat([backgroundColor[0], backgroundColor[1], backgroundColor[2], backgroundColor[3]]);
}
// var newarray = new Uint8Array(4*w*h);
var array = [];
for (var n = oy; n < oy + h; n++) {
var offsetValue = 4 * w * n;
if(n < ih){
var start = n * 4 * iw + ox * 4;
var end = n * 4 * iw + ox * 4 + 4 * w;
var pushArray = Array.from(pixels.data.slice(start, end ));
array.push.apply(array, pushArray);
} else {
array.push.apply(array, backgroundArray);
}
}
var newarray = Uint8Array.from(array);
pixels.data = newarray;
pixels.shape = [w, h, 4];
pixels.stride[1] = 4 * w;
options.format = input.format; const bg = options.backgroundColor.replace('rgba', '').replace('(', '').replace(')', '').split(',');
var chunks = []; let iw = pixels.shape[0], // Width of Original Image
var totalLength = 0; ih = pixels.shape[1], // Height of Original Image
var r = savePixels(pixels, options.format); offsetX,
offsetY,
w,
h;
r.on('data', function(chunk){ // Parse the inputs
totalLength += chunk.length; parseCornerCoordinateInputs({iw, ih},
chunks.push(chunk); {
x: { valInp: options.x, type: 'horizontal' },
y: { valInp: options.y, type: 'vertical' },
w: { valInp: options.w, type: 'horizontal' },
h: { valInp: options.h, type: 'vertical' },
}, function (opt, coord) {
offsetX = Math.floor(coord.x.valInp);
offsetY = Math.floor(coord.y.valInp);
w = Math.floor(coord.w.valInp);
h = Math.floor(coord.h.valInp);
}); });
r.on('end', function(){ const newPixels = new ndarray([], [w, h, 4]);
var data = Buffer.concat(chunks, totalLength).toString('base64');
var datauri = 'data:image/' + options.format + ';base64,' + data; for (let x = 0; x < w; x++) {
callback(datauri, options.format); for (let y = 0; y < h; y++) {
}); pixelSetter(x, y, bg, newPixels); // Set the background color
}); }
}
for (
let x = 0;
x < Math.min(w - 1, offsetX + iw - 1);
x++
) {
for (
let y = 0;
y < Math.min(h - 1, offsetY + ih - 1);
y++
) {
const inputImgX = x + offsetX,
inputImgY = y + offsetY;
pixelSetter(x, y, [
pixels.get(inputImgX, inputImgY, 0),
pixels.get(inputImgX, inputImgY, 1),
pixels.get(inputImgX, inputImgY, 2),
pixels.get(inputImgX, inputImgY, 3)
], newPixels); // Set the background color
}
}
if (cb) cb();
return newPixels;
}; };

View File

@@ -1,3 +1,4 @@
const pixelManipulation = require('../_nomodule/PixelManipulation');
/* /*
* Image Cropping module * Image Cropping module
* Usage: * Usage:
@@ -26,54 +27,35 @@ module.exports = function CropModule(options, UI) {
var step = this; var step = this;
// save the input image; function extraManipulation(pixels) {
// TODO: this should be moved to module API to persist the input image const newPixels = require('./Crop')(pixels, options, function() {
options.step.input = input.src; // We should do this via event/listener:
var parseCornerCoordinateInputs = require('../../util/ParseInputCoordinates'); if (ui && ui.hide) ui.hide();
//parse the inputs // Start custom UI setup (draggable UI)
parseCornerCoordinateInputs(options, { // Only once we have an input image
src: input.src, if (setupComplete === false && options.step.inBrowser && !options.noUI) {
x: { valInp: options.x, type: 'horizontal' }, setupComplete = true;
y: { valInp: options.y, type: 'vertical' }, ui.setup();
w: { valInp: options.w, type: 'horizontal' }, }
h: { valInp: options.h, type: 'vertical' }, });
}, function (options, coord) { return newPixels;
options.x = parseInt(coord.x.valInp); }
options.y = parseInt(coord.y.valInp);
options.w = coord.w.valInp; function output(image, datauri, mimetype, wasmSuccess) {
options.h = coord.h.valInp; step.output = { src: datauri, format: mimetype, wasmSuccess, useWasm: options.useWasm };
}
return pixelManipulation(input, {
output: output,
ui: options.step.ui,
extraManipulation: extraManipulation,
format: input.format,
image: options.image,
inBrowser: options.inBrowser,
callback: callback,
useWasm:options.useWasm
}); });
require('./Crop')(input, options, function (out, format) {
// This output is accessible to Image Sequencer
step.output = {
src: out,
format: format
};
// This output is accessible to the UI
options.step.output = out;
// Tell the UI that the step has been drawn
UI.onComplete(options.step);
// we should do this via event/listener:
if (ui && ui.hide) ui.hide();
// start custom UI setup (draggable UI)
// only once we have an input image
if (setupComplete === false && options.step.inBrowser && !options.noUI) {
setupComplete = true;
ui.setup();
}
// Tell Image Sequencer that step has been drawn
callback();
});
} }
return { return {

View File

@@ -16,12 +16,12 @@
"w": { "w": {
"type": "string", "type": "string",
"desc": "Width of crop", "desc": "Width of crop",
"default": "(50%)" "default": "50%"
}, },
"h": { "h": {
"type": "string", "type": "string",
"desc": "Height of crop", "desc": "Height of crop",
"default": "(50%)" "default": "50%"
}, },
"backgroundColor": { "backgroundColor": {
"type": "text", "type": "text",

View File

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

View File

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

View File

@@ -4,22 +4,24 @@ module.exports = exports = function(pixels, options){
options.startingX = options.startingX || defaults.startingX; options.startingX = options.startingX || defaults.startingX;
options.startingY = options.startingY || defaults.startingY; options.startingY = options.startingY || defaults.startingY;
var ox = Number(options.startingX), var ox = Number(options.startingX),
oy = Number(options.startingY), oy = Number(options.startingY),
iw = pixels.shape[0], iw = pixels.shape[0],
ih = pixels.shape[1], ih = pixels.shape[1],
thickness = Number(options.thickness) || defaults.thickness, thickness = Number(options.thickness) || defaults.thickness,
ex = options.endX = Number(options.endX) - thickness || iw - 1, ex = Number(options.endX) - thickness || iw - 1,
ey = options.endY = Number(options.endY) - thickness || ih - 1, ey = Number(options.endY) - thickness || ih - 1,
color = options.color || defaults.color; color = options.color || defaults.color;
color = color.substring(color.indexOf('(') + 1, color.length - 1); // extract only the values from rgba(_,_,_,_)
color = color.substring(color.indexOf('(') + 1, color.length - 1); // Extract only the values from rgba(_,_,_,_)
color = color.split(','); color = color.split(',');
var drawSide = function(startX, startY, endX, endY){ var drawSide = function(startX, startY, endX, endY){
for (var n = startX; n <= endX + thickness; n++){ for (var n = startX; n <= endX + thickness; n++){
for (var k = startY; k <= endY + thickness; k++){ for (var k = startY; k <= endY + thickness; k++){
pixelSetter(n, k, [color[0], color[1], color[2]], pixels); //to remove 4th channel - pixels.set(n, k, 3, color[3]); pixelSetter(n, k, [color[0], color[1], color[2]], pixels); // To remove 4th channel - pixels.set(n, k, 3, color[3]);
} }
} }
}; };

View File

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

View File

@@ -1,4 +1,5 @@
/** const Blur = require('../Blur/Blur');
/*
* Detect Edges in an Image * Detect Edges in an Image
* Uses Canny method for the same * Uses Canny method for the same
* Read more: https://en.wikipedia.org/wiki/Canny_edge_detector * Read more: https://en.wikipedia.org/wiki/Canny_edge_detector
@@ -21,42 +22,31 @@ module.exports = function edgeDetect(options, UI) {
var step = this; var step = this;
// Blur the image. // Makes the image greyscale
const internalSequencer = ImageSequencer({ inBrowser: false, ui: false }); function changePixel(r, g, b, a) {
return internalSequencer.loadImage(input.src, function() { return [(r + g + b) / 3, (r + g + b) / 3, (r + g + b) / 3, a];
internalSequencer.importJSON([{ 'name': 'blur', 'options': { blur: options.blur } }]); // Blurs the image before detecting edges to reduce noise. }
return internalSequencer.run(function onCallback(internalOutput) {
require('get-pixels')(internalOutput, function(err, blurPixels) {
if (err) {
return;
}
// Extra Manipulation function used as an enveloper for applying gaussian blur and Convolution. // Extra Manipulation function used as an enveloper for applying gaussian blur and Convolution
function changePixel(r, g, b, a) { function extraManipulation(pixels) {
return [(r + g + b) / 3, (r + g + b) / 3, (r + g + b) / 3, a]; const blurPixels = Blur(pixels, options.blur);
} return require('./EdgeUtils')(blurPixels, options.highThresholdRatio, options.lowThresholdRatio, options.hysteresis);
}
function extraManipulation() { function output(image, datauri, mimetype, wasmSuccess) {
return require('./EdgeUtils')(blurPixels, options.highThresholdRatio, options.lowThresholdRatio, options.hysteresis); step.output = { src: datauri, format: mimetype, wasmSuccess, useWasm: options.useWasm };
} }
function output(image, datauri, mimetype, wasmSuccess) { return require('../_nomodule/PixelManipulation.js')(input, {
step.output = { src: datauri, format: mimetype, wasmSuccess, useWasm: options.useWasm }; output: output,
} ui: options.step.ui,
changePixel: changePixel,
return require('../_nomodule/PixelManipulation.js')(input, { extraManipulation: extraManipulation,
output: output, format: input.format,
ui: options.step.ui, image: options.image,
changePixel: changePixel, inBrowser: options.inBrowser,
extraManipulation: extraManipulation, callback: callback,
format: input.format, useWasm: options.useWasm
image: options.image,
inBrowser: options.inBrowser,
callback: callback,
useWasm: options.useWasm
});
});
});
}); });
} }

View File

@@ -1,3 +1,4 @@
const _ = require('lodash');
/* /*
* Flip the image on vertical/horizontal axis. * Flip the image on vertical/horizontal axis.
*/ */
@@ -5,8 +6,7 @@ module.exports = function FlipImage(options, UI) {
var defaults = require('./../../util/getDefaults.js')(require('./info.json')); var defaults = require('./../../util/getDefaults.js')(require('./info.json'));
options.Axis = options.Axis || defaults.Axis; options.Axis = options.Axis || defaults.Axis;
var output, let output;
getPixels = require('get-pixels');
function draw(input, callback, progressObj) { function draw(input, callback, progressObj) {
@@ -15,35 +15,30 @@ module.exports = function FlipImage(options, UI) {
var step = this; var step = this;
return getPixels(input.src, function(err, oldPixels) { function changePixel(r, g, b, a) {
function changePixel(r, g, b, a) { return [r, g, b, a];
return [r, g, b, a]; }
} function extraManipulation(pixels) {
function extraManipulation(pixels) { const oldPixels = _.cloneDeep(pixels);
if (err) {
console.log(err);
return;
}
return require('./flipImage')(oldPixels, pixels, options.Axis);
}
function output(image, datauri, mimetype, wasmSuccess) {
step.output = { src: datauri, format: mimetype, wasmSuccess, useWasm: options.useWasm };
}
return require('../_nomodule/PixelManipulation.js')(input, { return require('./flipImage')(oldPixels, pixels, options.Axis);
output: output, }
ui: options.step.ui,
changePixel: changePixel, function output(image, datauri, mimetype, wasmSuccess) {
extraManipulation: extraManipulation, step.output = { src: datauri, format: mimetype, wasmSuccess, useWasm: options.useWasm };
format: input.format, }
image: options.image,
inBrowser: options.inBrowser, return require('../_nomodule/PixelManipulation.js')(input, {
callback: callback, output: output,
useWasm:options.useWasm ui: options.step.ui,
}); changePixel: changePixel,
extraManipulation: extraManipulation,
format: input.format,
image: options.image,
inBrowser: options.inBrowser,
callback: callback,
useWasm:options.useWasm
}); });
} }
return { return {

View File

@@ -1,52 +1,41 @@
module.exports = function Invert(options, UI) { const pixelSetter = require('../../util/pixelSetter.js'),
const pixelSetter = require('../../util/pixelSetter.js'); pixelManipulation = require('../_nomodule/PixelManipulation');
module.exports = function Gradient(options, UI) {
var output; var output;
// The function which is called on every draw. // The function which is called on every draw.
function draw(input, callback) { function draw(input, callback) {
var getPixels = require('get-pixels');
var savePixels = require('save-pixels');
var step = this; var step = this;
getPixels(input.src, function(err, pixels) {
if (err) {
console.log('Bad Image path');
return;
}
var width = pixels.shape[0];
for (var i = 0; i < pixels.shape[0]; i++) {
for (var j = 0; j < pixels.shape[1]; j++) {
let val = (i / width) * 255;
pixelSetter(i, j, [val, val, val, 255], pixels);
}
}
var chunks = [];
var totalLength = 0;
var r = savePixels(pixels, input.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/' + input.format + ';base64,' + data;
output(input.image, datauri, input.format);
callback();
});
});
function output(image, datauri, mimetype, wasmSuccess) { function output(image, datauri, mimetype, wasmSuccess) {
step.output = { src: datauri, format: mimetype, wasmSuccess, useWasm: options.useWasm }; step.output = { src: datauri, format: mimetype, wasmSuccess, useWasm: options.useWasm };
} }
function extraManipulation(pixels) {
const [w, h] = pixels.shape;
for (var i = 0; i < w; i++) {
for (var j = 0; j < h; j++) {
let val = (i / w) * 255;
pixelSetter(i, j, [val, val, val, 255], pixels);
}
}
return pixels;
}
return pixelManipulation(input, {
output,
extraManipulation,
callback,
format: input.format,
image: options.image,
inBrowser: options.inBrowser,
useWasm:options.useWasm
});
} }
return { return {

View File

@@ -1,3 +1,4 @@
const pixelManipulation = require('../_nomodule/PixelManipulation');
/* /*
* Invert the image * Invert the image
*/ */
@@ -21,7 +22,7 @@ function Invert(options, UI) {
step.output = { src: datauri, format: mimetype, wasmSuccess, useWasm: options.useWasm }; step.output = { src: datauri, format: mimetype, wasmSuccess, useWasm: options.useWasm };
} }
return input.pixelManipulation({ return pixelManipulation(input, {
output: output, output: output,
changePixel: changePixel, changePixel: changePixel,
format: input.format, format: input.format,
@@ -40,10 +41,4 @@ function Invert(options, UI) {
UI: UI UI: UI
}; };
} }
var info = { module.exports = Invert;
'name': 'invert',
'description': 'Inverts the image.',
'inputs': {
}
};
module.exports = [Invert, info];

View File

@@ -0,0 +1,4 @@
module.exports = [
require('./Module'),
require('./info.json')
];

View File

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

View File

@@ -18,16 +18,6 @@ module.exports = function Dynamic(options, UI, util) {
var parseCornerCoordinateInputs = require('../../util/ParseInputCoordinates'); var parseCornerCoordinateInputs = require('../../util/ParseInputCoordinates');
//parse the inputs
parseCornerCoordinateInputs(options, {
src: input.src,
x: { valInp: options.x, type: 'horizontal' },
y: { valInp: options.y, type: 'vertical' },
}, function(options, input) {
options.x = parseInt(input.x.valInp);
options.y = parseInt(input.y.valInp);
});
// save the pixels of the base image // save the pixels of the base image
var baseStepImage = this.getStep(options.offset).image; var baseStepImage = this.getStep(options.offset).image;
var baseStepOutput = this.getOutput(options.offset); var baseStepOutput = this.getOutput(options.offset);
@@ -35,6 +25,19 @@ module.exports = function Dynamic(options, UI, util) {
var getPixels = require('get-pixels'); var getPixels = require('get-pixels');
getPixels(input.src, function(err, pixels) { getPixels(input.src, function(err, pixels) {
// parse the inputs
parseCornerCoordinateInputs({
iw: pixels.shape[0],
ih: pixels.shape[1]
},
{
x: { valInp: options.x, type: 'horizontal' },
y: { valInp: options.y, type: 'vertical' },
}, function(opt, input) {
options.x = parseInt(input.x.valInp);
options.y = parseInt(input.y.valInp);
});
options.secondImagePixels = pixels; options.secondImagePixels = pixels;
function changePixel(r1, g1, b1, a1, x, y) { function changePixel(r1, g1, b1, a1, x, y) {

View File

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

View File

@@ -1,59 +1,71 @@
const imagejs = require('imagejs'),
pixelSetter = require('../../util/pixelSetter'),
ndarray = require('ndarray');
/* /*
* Resize the image by given percentage value * Resize the image by given percentage value
*/ */
module.exports = function Resize(options, UI) { module.exports = function Resize(options, UI) {
var output; let output;
function draw(input, callback, progressObj) { function draw(input, callback, progressObj) {
var defaults = require('./../../util/getDefaults.js')(require('./info.json')); const defaults = require('./../../util/getDefaults.js')(require('./info.json'));
options.resize = options.resize || defaults.resize; options.resize = options.resize || defaults.resize;
progressObj.stop(true); progressObj.stop(true);
progressObj.overrideFlag = true; progressObj.overrideFlag = true;
var step = this; const step = this;
var imagejs = require('imagejs');
function changePixel(r, g, b, a) {
return [r, g, b, a];
}
function extraManipulation(pixels) { function extraManipulation(pixels) {
// value above 100% scales up, and below 100% scales down // Value above 100% scales up, and below 100% scales down
var resize_value = parseInt(options.resize.slice(0, -1)); const resize_value = parseInt(options.resize.slice(0, -1));
var new_width, if (resize_value == 100) return pixels;
new_height;
new_width = Math.round(pixels.shape[0] * (resize_value / 100));
new_height = Math.round(pixels.shape[1] * (resize_value / 100));
var bitmap = new imagejs.Bitmap({ width: pixels.shape[0], height: pixels.shape[1] });
bitmap._data.data = pixels.data;
var resized = bitmap.resize({ const new_width = Math.round(pixels.shape[0] * (resize_value / 100)),
width: new_width, height: new_height, new_height = Math.round(pixels.shape[1] * (resize_value / 100));
const bitmap = new imagejs.Bitmap({ width: pixels.shape[0], height: pixels.shape[1] });
for (let x = 0; x < pixels.shape[0]; x++) {
for (let y = 0; y < pixels.shape[1]; y++) {
let r = pixels.get(x, y, 0),
g = pixels.get(x, y, 1),
b = pixels.get(x, y, 2),
a = pixels.get(x, y, 3);
bitmap.setPixel(x, y, r, g, b, a);
}
}
const resized = bitmap.resize({
width: new_width,
height: new_height,
algorithm: 'bicubicInterpolation' algorithm: 'bicubicInterpolation'
}); });
pixels.data = resized._data.data; const newPix = new ndarray([], [new_width, new_height, 4]);
pixels.shape = [new_width, new_height, 4];
pixels.stride[1] = 4 * new_width;
return pixels; for (let x = 0; x < new_width; x++) {
for (let y = 0; y < new_height; y++) {
const {r, g, b, a} = resized.getPixel(x, y);
pixelSetter(x, y, [r, g, b, a], newPix);
}
}
return newPix;
} }
function output(image, datauri, mimetype, wasmSuccess) { function output(image, datauri, mimetype, wasmSuccess) {
step.output = { src: datauri, format: mimetype, wasmSuccess, useWasm: options.useWasm }; step.output = { src: datauri, format: mimetype, wasmSuccess, useWasm: options.useWasm };
} }
return require('../_nomodule/PixelManipulation.js')(input, { return require('../_nomodule/PixelManipulation.js')(input, {
output: output, output: output,
ui: options.step.ui, ui: options.step.ui,
changePixel: changePixel,
extraManipulation: extraManipulation, extraManipulation: extraManipulation,
format: input.format, format: input.format,
image: options.image, image: options.image,

View File

@@ -3,34 +3,57 @@
*/ */
module.exports = function Rotate(options, UI) { module.exports = function Rotate(options, UI) {
var output; let output;
function draw(input, callback, progressObj) { function draw(input, callback, progressObj) {
var defaults = require('./../../util/getDefaults.js')(require('./info.json')); const defaults = require('./../../util/getDefaults.js')(require('./info.json'));
options.rotate = options.rotate || defaults.rotate; options.rotate = options.rotate || defaults.rotate;
progressObj.stop(true); progressObj.stop(true);
progressObj.overrideFlag = true; progressObj.overrideFlag = true;
var step = this; const step = this;
var imagejs = require('imagejs');
function changePixel(r, g, b, a) { function changePixel(r, g, b, a) {
return [r, g, b, a]; return [r, g, b, a];
} }
function extraManipulation(pixels) { function extraManipulation(pixels) {
var rotate_value = (options.rotate) % 360; const rotate_value = (options.rotate) % 360;
var radians = (Math.PI) * rotate_value / 180; radians = (Math.PI) * rotate_value / 180,
var width = pixels.shape[0]; width = pixels.shape[0],
var height = pixels.shape[1]; height = pixels.shape[1],
var cos = Math.cos(radians); cos = Math.cos(radians),
var sin = Math.sin(radians); sin = Math.sin(radians);
//final dimensions after rotation // Final dimensions after rotation
var pixels2 = require('ndarray')(new Uint8Array(4 * (Math.floor(Math.abs(width * cos) + Math.abs(height * sin) + 5) * (Math.floor(Math.abs(width * sin) + Math.abs(height * cos)) + 5))).fill(0), [Math.floor(Math.abs(width * cos) + Math.abs(height * sin)) + 5, Math.floor(Math.abs(width * sin) + Math.abs(height * cos)) + 4, 4]);
pixels = require('./Rotate')(pixels, pixels2, options, rotate_value, width, height, cos, sin); const finalPixels = require('ndarray')(
new Uint8Array(
4 *
(
Math.floor(
Math.abs(width * cos) +
Math.abs(height * sin) +
5
) *
(
Math.floor(
Math.abs(width * sin) +
Math.abs(height * cos)
) +
5
)
)
).fill(255),
[
Math.floor(Math.abs(width * cos) + Math.abs(height * sin)) + 5,
Math.floor(Math.abs(width * sin) + Math.abs(height * cos)) + 4,
4
]
);
pixels = require('./Rotate')(pixels, finalPixels, rotate_value, width, height, cos, sin);
return pixels; return pixels;
} }

View File

@@ -1,38 +1,81 @@
module.exports = function Rotate(pixels, pixels2, options, rotate_value, width, height, cos, sin){ const imagejs = require('imagejs'),
var imagejs = require('imagejs'); ndarray = require('ndarray'),
var height_half = Math.floor(height / 2); pixelSetter = require('../../util/pixelSetter');
var width_half = Math.floor(width / 2);
var dimension = width + height;
if (rotate_value % 360 == 0) module.exports = function Rotate(pixels, finalPixels, rotate_value, width, height, cos, sin){
return pixels; const height_half = Math.floor(height / 2),
function copyPixel(x1, y1, x2, y2,pixel_set,pixel_get){ width_half = Math.floor(width / 2);
pixel_set.set(x1, y1, 0, pixel_get.get(x2, y2, 0)); dimension = width + height;
pixel_set.set(x1, y1, 1, pixel_get.get(x2, y2, 1));
pixel_set.set(x1, y1, 2, pixel_get.get(x2, y2, 2)); if (rotate_value % 360 == 0) return pixels;
pixel_set.set(x1, y1, 3, pixel_get.get(x2, y2, 3));
function copyPixel(x1, y1, x2, y2, finalPix, initPix) {
finalPix.set(x1, y1, 0, initPix.get(x2, y2, 0));
finalPix.set(x1, y1, 1, initPix.get(x2, y2, 1));
finalPix.set(x1, y1, 2, initPix.get(x2, y2, 2));
finalPix.set(x1, y1, 3, initPix.get(x2, y2, 3));
} }
pixels1 = require('ndarray')(new Uint8Array(4 * dimension * dimension).fill(0), [dimension, dimension, 4]); const intermediatePixels = new ndarray(
//copying all the pixels from image to pixels1 new Uint8Array(4 * dimension * dimension).fill(255),
for (var n = 0; n < pixels.shape[0]; n++){ [dimension, dimension, 4]
for (var m = 0; m < pixels.shape[1]; m++){ ); // Intermediate ndarray of pixels with a greater size to prevent clipping.
copyPixel(n + height_half, m + width_half, n, m,pixels1,pixels);
// Copying all the pixels from image to intermediatePixels
for (let x = 0; x < pixels.shape[0]; x++){
for (let y = 0; y < pixels.shape[1]; y++){
copyPixel(x + height_half, y + width_half, x, y, intermediatePixels, pixels);
} }
} }
//rotating pixels1
var bitmap = new imagejs.Bitmap({ width: pixels1.shape[0], height: pixels1.shape[1] });
bitmap._data.data = pixels1.data;
var rotated = bitmap.rotate({ // Rotating intermediatePixels
const bitmap = new imagejs.Bitmap({ width: intermediatePixels.shape[0], height: intermediatePixels.shape[1] });
for (let x = 0; x < intermediatePixels.shape[0]; x++) {
for (let y = 0; y < intermediatePixels.shape[1]; y++) {
let r = intermediatePixels.get(x, y, 0),
g = intermediatePixels.get(x, y, 1),
b = intermediatePixels.get(x, y, 2),
a = intermediatePixels.get(x, y, 3);
bitmap.setPixel(x, y, r, g, b, a);
}
}
const rotated = bitmap.rotate({
degrees: rotate_value, degrees: rotate_value,
}); });
pixels1.data = rotated._data.data;
//cropping extra whitespace for (let x = 0; x < intermediatePixels.shape[0]; x++) {
for (var n = 0; n < pixels2.shape[0]; n++){ for (let y = 0; y < intermediatePixels.shape[1]; y++) {
for (var m = 0; m < pixels2.shape[1]; m++){ const {r, g, b, a} = rotated.getPixel(x, y);
copyPixel(n, m, n + Math.floor(dimension / 2 - Math.abs(width * cos / 2) - Math.abs(height * sin / 2)) - 1, m + Math.floor(dimension / 2 - Math.abs(height * cos / 2) - Math.abs(width * sin / 2)) - 1,pixels2,pixels1); pixelSetter(x, y, [r, g, b, a], intermediatePixels);
} }
} }
return pixels2;
// Cropping extra whitespace
for (let x = 0; x < finalPixels.shape[0]; x++){
for (let y = 0; y < finalPixels.shape[1]; y++){
copyPixel(
x,
y,
x +
Math.floor(
dimension / 2 -
Math.abs(width * cos / 2) -
Math.abs(height * sin / 2)
) - 1,
y +
Math.floor(
dimension / 2 -
Math.abs(height * cos / 2) -
Math.abs(width * sin / 2)
) - 1,
finalPixels,
intermediatePixels
);
}
}
return finalPixels;
}; };

View File

@@ -7,11 +7,9 @@ module.exports = function TextOverlay(options, UI) {
var step = this; var step = this;
var priorStep = this.getStep(-1); // get the previous step to add text onto it.
function extraManipulation(pixels) { function extraManipulation(pixels) {
//if (options.step.inBrowser) //if (options.step.inBrowser)
pixels = require('./TextOverlay')(pixels, options, priorStep); pixels = require('./TextOverlay')(pixels, options);
return pixels; return pixels;
} }

View File

@@ -1,7 +1,4 @@
module.exports = exports = function(pixels, options, priorstep){ module.exports = exports = function(pixels, options){
var $ = require('jquery'); // to make text-overlay work for node.js
var defaults = require('./../../util/getDefaults.js')(require('./info.json')); var defaults = require('./../../util/getDefaults.js')(require('./info.json'));
options.text = options.text || defaults.text; options.text = options.text || defaults.text;
@@ -11,20 +8,18 @@ module.exports = exports = function(pixels, options, priorstep){
options.color = options.color || defaults.color; options.color = options.color || defaults.color;
options.size = options.size || defaults.size; options.size = options.size || defaults.size;
var img = $(priorstep.imgElement);
if(Object.keys(img).length === 0){
img = $(priorstep.options.step.imgElement);
}
var canvas = document.createElement('canvas'); var canvas = document.createElement('canvas');
canvas.width = pixels.shape[0]; //img.width(); canvas.width = pixels.shape[0]; //img.width();
canvas.height = pixels.shape[1]; //img.height(); canvas.height = pixels.shape[1]; //img.height();
var ctx = canvas.getContext('2d'); var ctx = canvas.getContext('2d');
ctx.drawImage(img[0], 0, 0);
ctx.putImageData(new ImageData(new Uint8ClampedArray(pixels.data), pixels.shape[0], pixels.shape[1]), 0, 0);
ctx.fillStyle = options.color; ctx.fillStyle = options.color;
ctx.font = options.size + 'px ' + options.font; ctx.font = options.size + 'px ' + options.font;
ctx.fillText(options.text, options.x, options.y); ctx.fillText(options.text, options.x, options.y);
var myImageData = ctx.getImageData(0, 0, canvas.width, canvas.height); var myImageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
pixels.data = myImageData.data; pixels.data = new Uint8Array(myImageData.data);
return pixels; return pixels;
}; };

View File

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

View File

@@ -1,3 +1,10 @@
const pixelSetter = require('../../util/pixelSetter.js'),
getPixels = require('get-pixels'),
savePixels = require('save-pixels'),
ndarray = require('ndarray'),
gifshot = require('gifshot'),
fs = require('fs'),
path = require('path');
/* /*
* General purpose per-pixel manipulation * General purpose per-pixel manipulation
* accepting a changePixel() method to remix a pixel's channels * accepting a changePixel() method to remix a pixel's channels
@@ -13,9 +20,24 @@
module.exports = function PixelManipulation(image, options) { module.exports = function PixelManipulation(image, options) {
// To handle the case where pixelmanipulation is called on the input object itself // To handle the case where pixelmanipulation is called on the input object itself
// like input.pixelManipulation(options) // like input.pixelManipulation(options)
const pixelSetter = require('../../util/pixelSetter.js'); const isGIF = image.src.includes('image/gif');
let wasmSuccess; // Whether wasm succeded or failed let numFrames = 1, // Number of frames: 1 for a still image
frames = [], // Ndarray of pixels of each frame
perFrameShape, // Width, Height and color chanels of each frame
wasmSuccess, // Whether wasm succeded or failed
renderableFrames, // To block rendering in async modules
resolvedFrames = 0; // Number of WASM promises resolved.
/**
* @description Sets the render state of the current frame. True -> Renderable and False -> Not Renderable.
* @param {Boolean} state Render state of the frame
* @returns {Number} Total number of renderable frames.
*/
function setRenderState(state) {
renderableFrames += state ? 1 : -1;
return renderableFrames;
}
if (arguments.length <= 1) { if (arguments.length <= 1) {
options = image; options = image;
@@ -24,146 +46,278 @@ module.exports = function PixelManipulation(image, options) {
options = options || {}; options = options || {};
const getPixels = require('get-pixels'), /**
savePixels = require('save-pixels'); * @description Returns the DataURI of an image from its pixels
* @param {"ndarray"} pix pixels ndarray of pixels of the image.
* @param {String} format Format/MimeType of the image input.
* @returns {Promise} Promise with DataURI as parameter in the callback.
*/
const getDataUri = (pix, format) => {
return new Promise(resolve => {
let chunks = [],
totalLength = 0;
let r = savePixels(pix, format, {
quality: 100
});
r.on('data', function (chunk) {
totalLength += chunk.length;
chunks.push(chunk);
});
r.on('end', function () {
let data = Buffer.concat(chunks, totalLength).toString('base64');
let datauri = 'data:image/' + format + ';base64,' + data;
resolve(datauri);
});
});
};
getPixels(image.src, function (err, pixels) { getPixels(image.src, function (err, pixels) {
if (err) { if (err) {
console.log('Bad image path', image); console.log('get-pixels error: ', err);
return; return;
} }
if (options.getNeighbourPixel) { // There may be a more efficient means to encode an image object,
options.getNeighbourPixel.fun = function getNeighborPixel(distX, distY) { // but node modules and their documentation are essentially arcane on this point.
return options.getNeighbourPixel(pixels, x, y, distX, distY); function generateOutput() {
}; if (!(renderableFrames < numFrames) && !(resolvedFrames < numFrames)) {
}
// Iterate through pixels if (isGIF) {
// TODO: this could possibly be more efficient; see const dataPromises = []; // Array of all DataURI promises
// https://github.com/p-v-o-s/infragram-js/blob/master/public/infragram.js#L173-L181
if (options.preProcess) pixels = options.preProcess(pixels); // Allow for preprocessing of pixels.
function extraOperation() {
var res;
if (options.extraManipulation) res = options.extraManipulation(pixels, generateOutput); // extraManipulation is used to manipulate each pixel individually.
// 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, wasmSuccess);
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 */
const imports = {
env: {
consoleLog: console.log,
perform: function (x, y) {
let pixel = options.changePixel( // changePixel function is run over every pixel.
pixels.get(x, y, 0),
pixels.get(x, y, 1),
pixels.get(x, y, 2),
pixels.get(x, y, 3),
x,
y
);
pixelSetter(x, y, pixel, pixels);
for (let f = 0; f < numFrames; f++) {
dataPromises.push(getDataUri(frames[f], options.format));
} }
}
};
function perPixelManipulation() { Promise.all(dataPromises).then(datauris => {
for (var x = 0; x < pixels.shape[0]; x++) { const gifshotOptions = {
for (var y = 0; y < pixels.shape[1]; y++) { images: datauris,
imports.env.perform(x, y); frameDuration: 1, // Duration of each frame in 1/10 seconds.
} numFrames: datauris.length,
} gifWidth: perFrameShape[0],
} gifHeight: perFrameShape[1]
};
const gifshotCb = out => {
if (out.error) {
console.log('gifshot error: ', out.errorMsg);
}
if (options.output)
options.output(options.image, out.image, 'gif', wasmSuccess);
if (options.callback) options.callback();
};
const inBrowser = (options.inBrowser) ? 1 : 0; if (options.inBrowser) {
const test = (process.env.TEST) ? 1 : 0; gifshot.createGIF(gifshotOptions, gifshotCb);
}
if (options.useWasm) { else {
if (options.inBrowser) { const nodejsGIFShot = eval('require')('./node-gifshot');
nodejsGIFShot(gifshotOptions, gifshotCb);
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);
wasmSuccess = true;
extraOperation();
}).catch(err => {
console.log(err);
console.log('WebAssembly acceleration errored; falling back to JavaScript in PixelManipulation');
wasmSuccess = false;
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);
wasmSuccess = true;
extraOperation(); }
else {
getDataUri(frames[0], options.format).then(datauri => {
if (options.output)
options.output(options.image, datauri, options.format, wasmSuccess);
if (options.callback) options.callback();
});
}
}
}
// Get pixels of each frame
if (isGIF) {
const { shape } = pixels;
const [
noOfFrames,
width,
height,
channels
] = shape;
numFrames = noOfFrames;
renderableFrames = noOfFrames; // Total number of renderable frames (mutable)
perFrameShape = [width, height, channels]; // Shape of ndarray of each frame
const numPixelsInFrame = width * height;
/* Coalesce the GIF frames (Some GIFs store delta information in between frames
i.e. Only the pixels which change between frames are stored. All these frames need to be
"Coalesced" to get final GIF frame.
More Info: https://www.npmjs.com/package/gif-extract-frames#why
*/
// Credit for the below code: https://www.npmjs.com/package/gif-extract-frames
// We couldn't use the library because it uses ES6 features which cannot be browserified
for (let i = 0; i < numFrames; ++i) {
if (i > 0) {
const currIndex = pixels.index(i, 0, 0, 0);
const prevIndex = pixels.index(i - 1, 0, 0, 0);
for (let j = 0; j < numPixelsInFrame; ++j) {
const curr = currIndex + j * channels;
if (pixels.data[curr + channels - 1] === 0) {
const prev = prevIndex + j * channels;
for (let k = 0; k < channels; ++k) {
pixels.data[curr + k] = pixels.data[prev + k];
}
}
}
}
}
for (let f = 0; f < numFrames; f++) {
frames.push(
new ndarray(
new Uint8Array(
perFrameShape[0] *
perFrameShape[1] *
perFrameShape[2]
),
perFrameShape
)
);
for (let x = 0; x < width; x++) {
for (let y = 0; y < height; y++) {
for (let c = 0; c < channels; c++) {
frames[f].set(x, y, c, pixels.get(f, x, y, c));
}
}
}
}
}
else {
frames.push(pixels);
}
// Manipulate every frame separately
for (let f = 0; f < numFrames; f++) {
let framePix = frames[f];
if (options.getNeighbourPixel) {
options.getNeighbourPixel.fun = function getNeighborPixel(distX, distY) {
return options.getNeighbourPixel(framePix, x, y, distX, distY);
};
}
// Iterate through framePix
// 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.preProcess){
frames[f] = options.preProcess(framePix, setRenderState) || framePix; // Allow for preprocessing of framePix.
perFrameShape = frames[f].shape;
}
if (options.changePixel) {
/* Allows for Flexibility
if per pixel manipulation is not required */
const imports = {
env: {
consoleLog: console.log,
perform: function (x, y) {
let pixel = options.changePixel( // changePixel function is run over every pixel.
framePix.get(x, y, 0),
framePix.get(x, y, 1),
framePix.get(x, y, 2),
framePix.get(x, y, 3),
x,
y
);
pixelSetter(x, y, pixel, framePix);
}
}
};
/**
* @description Pure JS pixelmanipulation fallback when WASM is not working.
*/
function perPixelManipulation() {
for (var x = 0; x < framePix.shape[0]; x++) {
for (var y = 0; y < framePix.shape[1]; y++) {
imports.env.perform(x, y);
}
}
}
const inBrowser = (options.inBrowser) ? 1 : 0;
const test = (process.env.TEST) ? 1 : 0;
if (options.useWasm) {
if (options.inBrowser) {
fetch('../../../dist/manipulation.wasm').then(response =>
response.arrayBuffer()
).then(bytes =>
WebAssembly.instantiate(bytes, imports)
).then(results => {
results.instance.exports.manipulatePixel(framePix.shape[0], framePix.shape[1], inBrowser, test);
wasmSuccess = true;
resolvedFrames++;
generateOutput();
}).catch(err => {
console.log(err);
console.log('WebAssembly acceleration errored; falling back to JavaScript in PixelManipulation');
perPixelManipulation();
wasmSuccess = false;
resolvedFrames++;
generateOutput();
}); });
} }
catch(err){ else {
console.log(err); try{
console.log('WebAssembly acceleration errored; falling back to JavaScript in PixelManipulation'); const wasmPath = path.join(__dirname, '../../../', 'dist', 'manipulation.wasm');
wasmSuccess = false; const buf = fs.readFileSync(wasmPath);
WebAssembly.instantiate(buf, imports).then(results => {
perPixelManipulation(); results.instance.exports.manipulatePixel(framePix.shape[0], framePix.shape[1], inBrowser, test);
extraOperation();
wasmSuccess = true;
resolvedFrames++;
generateOutput();
});
}
catch(err){
console.log(err);
console.log('WebAssembly acceleration errored; falling back to JavaScript in PixelManipulation');
perPixelManipulation();
wasmSuccess = false;
resolvedFrames++;
generateOutput();
}
} }
} }
} else { else {
wasmSuccess = false; perPixelManipulation();
perPixelManipulation(); wasmSuccess = false;
extraOperation(); resolvedFrames++;
generateOutput();
}
} }
else resolvedFrames++;
if (options.extraManipulation){
frames[f] = options.extraManipulation(framePix, setRenderState, generateOutput) || framePix; // extraManipulation is used to manipulate each pixel individually.
perFrameShape = frames[f].shape;
}
generateOutput();
} }
}); });
}; };

View File

@@ -0,0 +1,47 @@
/**
* @param {Object} options GIFShot options object
* @param {Function} cb GIFShot callback
* @returns {null}
*/
function nodejsGIFShot(options, cb) {
const puppeteer = eval('require')('puppeteer');
puppeteer.launch(
{
headless: true,
args:['--no-sandbox', '--disable-setuid-sandbox']
}
)
.then(browser => {
browser.newPage().then(page => {
page.goto('about:blank').then(() => {
page.addScriptTag({ path: require('path').join(__dirname, '../../../node_modules/gifshot/dist/gifshot.min.js') })
.then(() => {
page.evaluate(options => {
return new Promise(resolve => {
gifshot.createGIF(options, resolve);
});
},
options
)
.then(obj => {
browser.close().then(() => {
if (cb) cb(obj);
});
})
.catch(e => {
console.log('Puppeteer error: ', e);
browser.close().then(() => {
if (cb) cb({
error: true,
errorMsg: e
});
});
});
});
});
});
});
}
module.exports = nodejsGIFShot;

View File

@@ -1,24 +1,23 @@
module.exports = function parseCornerCoordinateInputs(options, coord, callback) { module.exports = function parseCornerCoordinateInputs(options, coord, callback) {
var getPixels = require('get-pixels'); const {iw, ih} = options;
getPixels(coord.src, function(err, pixels) {
var iw = pixels.shape[0], function convert(key) {
ih = pixels.shape[1]; var val = coord[key];
if (!coord.x.valInp) {
return; val.valInp = val.valInp.toString();
val.valInp = val.valInp.replace(/[\)\(]/g, '');
if (val.valInp && val.valInp.slice(-1) === '%') {
val.valInp = val.valInp.replace('%', '');
val.valInp = parseInt(val.valInp);
if (val.type === 'horizontal') val.valInp = val.valInp * iw / 100;
else val.valInp = val.valInp * ih / 100;
} }
else { else val.valInp = parseInt(val.valInp);
Object.keys(coord).forEach(convert); }
function convert(key) {
var val = coord[key]; Object.keys(coord).forEach(convert);
if (val.valInp && val.valInp.slice(-1) === '%') { callback(options, coord);
val.valInp = parseInt(val.valInp, 10);
if (val.type === 'horizontal')
val.valInp = val.valInp * iw / 100;
else
val.valInp = val.valInp * ih / 100;
}
}
}
callback(options, coord);
});
}; };

View File

@@ -1,10 +1,29 @@
const getPixels = require('get-pixels');
module.exports = function getImageDimensions(img, cb) { module.exports = function getImageDimensions(img, cb) {
var getPixels = require('get-pixels'); let dimensions;
var dimensions = { width: '', height: '' }; let isGIF;
getPixels(img, function(err, pixels) { getPixels(img, function(err, pixels) {
dimensions.width = pixels.shape[0]; if (pixels.shape.length === 4) {
dimensions.height = pixels.shape[1]; const [frames, width, height] = pixels.shape;
cb(dimensions); dimensions = {
frames,
width,
height
};
isGIF = true;
}
else {
const [width, height] = pixels.shape;
dimensions = {
width,
height
};
isGIF = false;
}
if (cb) cb(dimensions, isGIF);
}); });
}; };

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -42,19 +42,22 @@ function runBenchmarks(sequencer, t) {
function cb() { function cb() {
var end = Date.now(); var end = Date.now();
console.log('Module ' + mods[0] + ' ran in: ' + (end - global.start) + ' milliseconds'); 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; sequencer.steps[global.idx].output.src = image;
global.idx++; global.idx++;
if (mods[0] === 'import-image' || (!!sequencer.modulesInfo(mods[0]).requires && sequencer.modulesInfo(mods[0]).requires.includes('webgl'))) { mods.splice(0, 1);
while (mods[0] === 'import-image' || mods[0] === 'minify-image' || (!!sequencer.modulesInfo(mods[0]).requires && sequencer.modulesInfo(mods[0]).requires.includes('webgl'))) {
/* Not currently working for this module, which is for importing a new image */ /* Not currently working for this module, which is for importing a new image */
console.log('Bypassing import-image'); console.log(`Bypassing ${mods[0]}`);
mods.splice(0, 1); mods.splice(0, 1);
} }
sequencer.addSteps(mods[0]); sequencer.addSteps(mods[0]);
global.start = Date.now(); global.start = Date.now();
sequencer.run({ index: global.idx }, cb); sequencer.run({ index: global.idx }, cb);
} else { }
else {
t.end(); t.end();
} }
} }

View File

@@ -38,11 +38,11 @@ module.exports = (moduleName, options, benchmark, input) => {
sequencer.run({mode: 'test'}, () => { sequencer.run({mode: 'test'}, () => {
let result = sequencer.steps[1].output.src; let result = sequencer.steps[1].output.src;
base64Img.imgSync(result, target, 'result'); base64Img.imgSync(result, target, `${moduleName}-result`);
base64Img.imgSync(benchmark, target, 'benchmark'); base64Img.imgSync(benchmark, target, `${moduleName}-benchmark`);
result = './test_outputs/result.png'; result = `./test_outputs/${moduleName}-result.png`;
benchmark = './test_outputs/benchmark.png'; benchmark = `./test_outputs/${moduleName}-benchmark.png`;
looksSame(result, benchmark, function(err, res) { looksSame(result, benchmark, function(err, res) {
if (err) console.log(err); if (err) console.log(err);

View File

@@ -1,17 +1,18 @@
var test = require('tape'); var test = require('tape');
var red = 'data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAYEBQYFBAYGBQYHBwYIChAKCgkJChQODwwQFxQYGBcUFhYaHSUfGhsjHBYWICwgIyYnKSopGR8tMC0oMCUoKSj/2wBDAQcHBwoIChMKChMoGhYaKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCj/wAARCAAQABADASIAAhEBAxEB/8QAFQABAQAAAAAAAAAAAAAAAAAAAAf/xAAUEAEAAAAAAAAAAAAAAAAAAAAA/8QAFQEBAQAAAAAAAAAAAAAAAAAABgj/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwCdABykX//Z';
var parseCornerCoordinateInputs = require('../../../src/util/ParseInputCoordinates'); var parseCornerCoordinateInputs = require('../../../src/util/ParseInputCoordinates');
test('parseCornerCoordinateInputs works.', function (t) { test('parseCornerCoordinateInputs works.', function (t) {
var options = { x: '10%' }, var options = { x: '10%', iw: 10, ih: 10 },
coord = { src: red, x: { valInp: options.x, type: 'horizontal' } }; coord = { x: { valInp: options.x, type: 'horizontal' } };
callback = function (options, coord) { callback = function (options, coord) {
options.x = parseInt(coord.x.valInp); options.x = parseInt(coord.x.valInp);
t.equal(options.x, 1, 'parseCornerCoordinateInputs works.'); t.equal(options.x, 1, 'parseCornerCootesrdinateInputs Works.');
t.equal(typeof options.x, 'number', 'Correct output type');
t.end(); t.end();
}; };
parseCornerCoordinateInputs(options, coord, callback); parseCornerCoordinateInputs(options, coord, callback);
}); });