Merge Commit

This commit is contained in:
Chinmay Pandhare
2017-07-29 03:50:31 +05:30
28 changed files with 1292 additions and 937 deletions

260
CONTRIBUTING.md Normal file
View File

@@ -0,0 +1,260 @@
Contributing to Image Sequencer
===
Happily accepting pull requests; to edit the core library, modify files in `./src/`. To build, run `npm install` followed by `grunt build`.
Most contribution (we imagine) would be in the form of API-compatible modules, which need not be directly included.
## Contributing modules
Any module must look like this :
```js
module.exports = function ModuleName(options,UI) {
options = options || {};
options.title = "Title of the Module";
UI.onSetup();
var output;
function draw(input,callback) {
UI.onDraw();
var output = /*do something with the input*/ ;
this.output = output;
callback();
UI.onComplete(this.output.src);
}
return {
options: options,
draw: draw,
output: output,
UI: UI
}
}
```
### options
The object `options` stores some important information. This is how you can accept
input from users. If you require a variable "x" from the user and the user passes
it in, you will be able to access it as `options.x`.
Options also has some in-built properties. The `options.inBrowser` boolean denotes
whether your module is being run on a browser.
### draw()
The draw method is run every time the step is `run` using `sequencer.run()`.
So any calculations must go **into** the `draw()` method's definition.
What is not in the draw method, but is in the `module.exports` is executed only
when the step is added. So whatever external npm modules are to be loaded, or
constant definitions must be done **outside** the `draw()` method's definition.
`draw()` receives two arguments - `input` and `callback` :
* `input` is an object which is essentially the output of the previous step.
```js
input = {
src: "<$DataURL>",
format: "<png|jpeg|gif>"
}
```
* `callback` is a function which is responsible to tell the sequencer that the
step has been "drawn".
When you have done your calculations and produced an image output, you are required
to set `this.output` to an object similar to what the input object was, and call
`callback()`.
### UI Methods
The module is responsible to emit various events for the UI to capture. There are
four events in all:
* `UI.onSetup()` must be emitted when the module is added. So it must be emitted
outside the draw method's definition as shown above.
* `UI.onDraw()` must be emitted whenever the `draw()` method is called. So it should
ideally be the first line of the definition of the `draw` method.
* `UI.onComplete(output_src)` must be emitted whenever the output of a draw call
is ready. An argument, that is the DataURL of the output image must be passed in.
* `UI.onRemove()` is emitted automatically and the module should not emit it.
To add a module to Image Sequencer, it must have the following method; you can wrap an existing module to add them:
* `module.draw()`
The `draw(input,callback)` method should accept an `input` parameter, which will be an object of the form:
```js
input = {
src: "datauri here",
format: "jpeg/png/etc"
}
```
## options.title
For display in the web-based UI, each module may also have a title `options.title`.
### Module example
See existing module `green-channel` for an example: https://github.com/publiclab/image-sequencer/tree/master/src/modules/GreenChannel/Module.js
For help integrating, please open an issue.
****
## Development
Notes on development next steps:
### UI
* [ ] add createUserInterface() which is set up by default to draw on ImageBoardUI, but could be swapped for nothing, or an equiv. lib
* [ ] it could create the interface and use event listeners like module.on('draw', fn()); to update the interface
* [ ] spinners before panels are complete
* [ ] is there a module for generating forms from parameters?
* [ ] click to expand for all images
* [ ] `ImageSequencer.Renderer` class to manage image output formats and adapters
* [ ] remove step
* [ ] output besides an image -- like `message(txt)` to display to the step's UI
### Modularization
* [ ] remotely includable modules, not compiled in -- see plugin structures in other libs
* [x] ability to start running at any point -- already works?
* [x] commandline runnability?
* [x] Make available as browserified OR `require()` includable...
* [ ] standardize panel addition with submodule that offers Panel.display(image)
* [ ] allow passing data as data-uri or Image object, or stream, or ndarray or ImageData array, if both of neighboring pair has ability?
* see https://github.com/jywarren/image-sequencer/issues/1
* [ ] ...could we directly include package.json for module descriptions? At least as a fallback.
* [ ] (for node-and-line style UIs) non-linear sequences with Y-splitters
* [ ] `sequencer.addModule('path/to/module.js')` style module addition -- also to avoid browserifying all of Plotly :-P
* [x] remove step
### Testing
* [x] tests - modules headless; unit tests
* [ ] comparisons with diff
* [ ] testing a module's promised functionality: each module could offer before/after images as part of their API; by running the module on the before image, you should get exactly the after image, comparing with an image diff
### Use cases
* [ ] make an Infragram module that accepts a math expression
### Bugs
* [x] BUG: this doesn't work for defaults: imageboard.loadImage('examples/grid.png', function() {});
* we should make defaults a config of the first module
****
## Module Candidates
* https://github.com/linuxenko/rextract.js
* https://www.npmjs.com/package/histogram
* https://github.com/hughsk/flood-fill
* https://www.npmjs.com/package/blink-diff
* smaller and faster: https://www.npmjs.com/package/@schornio/pixelmatch
* https://github.com/yahoo/pngjs-image has lots of useful general-purpose image getters like `image.getLuminosityAtIndex(idx)`
* some way to add in a new image (respecting alpha) -- `add-image` (with blend mode, default `normal`?)
* https://github.com/yuta1984/CannyJS - edge detection
* http://codepen.io/taylorcoffelt/pen/EsCcr - more edge detection
## Ideas
* https://github.com/vicapow/jsqrcode
* https://github.com/jadnco/whirl - scrubbable image sequence player
* non graphics card GL functions could be shimmed with https://github.com/Overv/JSGL
* or this: https://github.com/stackgl/headless-gl
* https://github.com/mattdesl/fontpath-simple-renderer
* output in animated Gif? as a module
### Referencing earlier states
Complex sequences with masking could require accessing previous states (or nonlinearity):
* flood-fill an area
* select only the flooded area
* roundabout: lighten everything to <50%, then flood-fill with black? Not 100% reliable.
* roundabout 2: `flood fill`, then `blink-diff` with original
* then add step which recovers original image, repeat `flood-fill`/`blink-diff` for second region
* reference above masked states in a `mask` module, with `maskModule.draw(image, { getMask: function() { return maskImg } })`
****
**Notes:**
`pattern-fill` module to use patterns in JS canvas:
```js
var c=document.getElementById("myCanvas");
var ctx=c.getContext("2d");
var img=document.getElementById("lamp");
var pat=ctx.createPattern(img,"repeat");
ctx.rect(0,0,150,100);
ctx.fillStyle=pat;
ctx.fill();
```
Masking:
```js
ctx.save();
ctx.beginPath();
ctx.moveTo(0, 0);
ctx.lineTo(160, 600);
ctx.rect(0, 0, 160, 600);
ctx.closePath();
ctx.clip();
ctx.drawImage(img, 0, 0);
ctx.restore();
```
****
## UI notes:
* visual nodes-and-lines UI: https://github.com/flowhub/the-graph
* https://flowhub.github.io/the-graph/examples/demo-simple.html
```js
settings: {
'threshold': {
type: 'slider',
label: 'Threshold',
default: 50,
min: 0,
max: 100
},
'colors': {
type: 'select',
label: 'Colors',
options: [
{ name: '0', value: '0', default: true },
{ name: '1', value: '1' },
{ name: '2', value: '2' }
]
}
}
```
Possible web-based commandline interface: https://hyper.is/?
### Path cutting
* threshold
* vectorize
* edge detect
* direction find (vectorize and colorize)

272
README.md
View File

@@ -26,6 +26,14 @@ It is also for prototyping some other related ideas:
* [Basic example](https://jywarren.github.io/image-sequencer/) * [Basic example](https://jywarren.github.io/image-sequencer/)
* [NDVI example](https://jywarren.github.io/image-sequencer/examples/ndvi/) - related to [Infragram.org](http://infragram.org) * [NDVI example](https://jywarren.github.io/image-sequencer/examples/ndvi/) - related to [Infragram.org](http://infragram.org)
## Jump to:
* [Quick Usage](#quick-usage)
* [Classic Usage](#classic-usage)
* [Method Chaining](#method-chaining)
* [Multiple Images](#multiple-images)
* [Creating a User Interface](#creating-a-user-interface)
## Quick Usage ## Quick Usage
Image Sequencer can be used to run modules on an HTML Image Element using the Image Sequencer can be used to run modules on an HTML Image Element using the
@@ -68,11 +76,26 @@ a name and an image. The method also accepts an optional callback.
```js ```js
sequencer.loadImage(image_src,optional_callback); sequencer.loadImage(image_src,optional_callback);
``` ```
On `Node.js` the `image_src` may be a DataURI or a local path. On browsers, it On `Node.js` the `image_src` may be a DataURI or a local path or a URL.
must be a DatURI (or 'selector to image' -- Work in Progress)
return value: **`sequencer`** (To allow method chaining) On browsers, it may be a DatURI, a local image or a URL (Unless this violates
CORS Restrictions). To sum up, these are accepted:
* Images in the same domain (or directory - for a local implementation)
* CORS-Proof images in another domain.
* DataURLs
return value: **none** (A callback should be used to ensure the image gets loaded)
The callback is called within the scope of a the sequencer. For example:
(addSteps is defined later)
```js
sequencer.loadImage('SRC',function(){
this.addSteps('module-name');
});
```
The `this` refers to all the images added in the parent `loadImages` function only.
In this case, only `'SRC'`.
### Adding steps to the image ### Adding steps to the image
@@ -156,6 +179,7 @@ return value: **`sequencer`** (To allow method chaining)
## Method Chaining ## Method Chaining
Methods can be chained on the Image Sequencer: Methods can be chained on the Image Sequencer:
* loadImage()/loadImages() can only terminate a chain.
* run() can not be in the middle of the chain. * run() can not be in the middle of the chain.
* If the chain starts with loadImage() or loadImages(), the following methods are * If the chain starts with loadImage() or loadImages(), the following methods are
applied only to the newly loaded images. applied only to the newly loaded images.
@@ -164,9 +188,11 @@ be of the form "image<number>". For ex: "image1", "image2", "image3", etc.
Valid Chains: Valid Chains:
```js ```js
sequencer.loadImage('red').addSteps('invert').run(function(out){ sequencer.loadImage('red',function(){
//do something with otuput. this.addSteps('invert').run(function(out){
}); //do something with ouptut.
});
})
sequencer.addSteps(['ndvi-red','invert']).run(); sequencer.addSteps(['ndvi-red','invert']).run();
et cetra. et cetra.
``` ```
@@ -209,7 +235,7 @@ with each image. This is a string literal.
}); });
``` ```
return value: **`sequencer`** (To allow method chaining) return value: **none**
### Adding Steps on Multiple Images ### Adding Steps on Multiple Images
@@ -325,204 +351,50 @@ sequencer.insertSteps({
return value: **`sequencer`** (To allow method chaining) return value: **`sequencer`** (To allow method chaining)
## Contributing ## Creating a User Interface
Happily accepting pull requests; to edit the core library, modify files in `/src/`. To build, run `npm install` and `grunt build`. Image Sequencer provides the following events which can be used to generate a UI:
### Contributing modules * `onSetup` : this event is triggered when a new module is set up. This can be used,
for instance, to generate a DIV element to store the generated image for that step.
* `onDraw` : This event is triggered when Image Sequencer starts drawing the output
for a module. This can be used, for instance, to overlay a loading GIF over the DIV
generated above.
* `onComplete` : This event is triggered when Image Sequencer has drawn the output
for a module. This can be used, for instance, to update the DIV with the new image
and remove the loading GIF generated above.
* `onRemove` : This event is triggered when a module is removed. This can be used,
for instance, to remove the DIV generated above.
Most contribution (we imagine) would be in the form of API-compatible modules, which need not be directly included. How to define these functions:
#### draw()
To add a module to Image Sequencer, it must have the following method; you can wrap an existing module to add them:
* `module.draw()`
The `draw(input,callback)` method should accept an `input` parameter, which will be an object of the form:
```js ```js
input = { sequencer.setUI({
src: "datauri here", onSetup: function() {},
format: "jpeg/png/etc" onDraw: function() {},
onComplete: function(output) {},
onRemove: function() {}
});
```
These methods can be defined and re-defined at any time, but it is advisable to
set them before any module is added and not change it thereafter. This is because
the `setUI` method will only affect the modules added after `setUI` is called.
The `onComplete` event is passed on the output of the module.
In the scope of all these events, the following variables are present, which
may be used in generating the UI:
* The object `identity`
```
identity = {
stepName: "Name of the Step",
stepID: "A unique ID given to the step",
imageName: "The name of the image to which the step is added."
} }
``` ```
* The variable `options.inBrowser` which is a Boolean and is `true` if the client is a browser and `false` otherwise.
The `image` object is essentially the output of the previous step. Note: `identity.imageName` is the "name" of that particular image. This name can be specified
while loading the image via `sequencer.loadImage("name","SRC")`. If not specified,
The draw method must, when it is complete, pass the output image to the method `this.output = modified_input`, which will send the output to the next module in the chain. For example: the name of a loaded image defaults to a name like "image1", "image2", et cetra.
```js
function draw(image) {
// do some stuff with the image
this.output = image;
callback();
}
```
#### Title
For display in the web-based UI, each module may also have a title like `options.title`.
#### Module example
See existing module `green-channel` for an example: https://github.com/jywarren/image-sequencer/tree/master/src/modules/GreenChannel.js
For help integrating, please open an issue.
****
## Development
Notes on development next steps:
### UI
* [ ] add createUserInterface() which is set up by default to draw on ImageBoardUI, but could be swapped for nothing, or an equiv. lib
* [ ] it could create the interface and use event listeners like module.on('draw', fn()); to update the interface
* [ ] spinners before panels are complete
* [ ] is there a module for generating forms from parameters?
* [ ] click to expand for all images
* [ ] `ImageSequencer.Renderer` class to manage image output formats and adapters
* [ ] remove step
* [ ] output besides an image -- like `message(txt)` to display to the step's UI
### Modularization
* [ ] remotely includable modules, not compiled in -- see plugin structures in other libs
* [x] ability to start running at any point -- already works?
* [x] commandline runnability?
* [x] Make available as browserified OR `require()` includable...
* [ ] standardize panel addition with submodule that offers Panel.display(image)
* [ ] allow passing data as data-uri or Image object, or stream, or ndarray or ImageData array, if both of neighboring pair has ability?
* see https://github.com/jywarren/image-sequencer/issues/1
* [ ] ...could we directly include package.json for module descriptions? At least as a fallback.
* [ ] (for node-and-line style UIs) non-linear sequences with Y-splitters
* [ ] `sequencer.addModule('path/to/module.js')` style module addition -- also to avoid browserifying all of Plotly :-P
* [x] remove step
### Testing
* [ ] tests - modules headless; unit tests
* [ ] comparisons with diff
* [ ] testing a module's promised functionality: each module could offer before/after images as part of their API; by running the module on the before image, you should get exactly the after image, comparing with an image diff
### Use cases
* [ ] make an Infragram module that accepts a math expression
### Bugs
* [x] BUG: this doesn't work for defaults: imageboard.loadImage('examples/grid.png', function() {});
* we should make defaults a config of the first module
****
## Module Candidates
* https://github.com/linuxenko/rextract.js
* https://www.npmjs.com/package/histogram
* https://github.com/hughsk/flood-fill
* https://www.npmjs.com/package/blink-diff
* smaller and faster: https://www.npmjs.com/package/@schornio/pixelmatch
* https://github.com/yahoo/pngjs-image has lots of useful general-purpose image getters like `image.getLuminosityAtIndex(idx)`
* some way to add in a new image (respecting alpha) -- `add-image` (with blend mode, default `normal`?)
* https://github.com/yuta1984/CannyJS - edge detection
* http://codepen.io/taylorcoffelt/pen/EsCcr - more edge detection
## Ideas
* https://github.com/vicapow/jsqrcode
* https://github.com/jadnco/whirl - scrubbable image sequence player
* non graphics card GL functions could be shimmed with https://github.com/Overv/JSGL
* or this: https://github.com/stackgl/headless-gl
* https://github.com/mattdesl/fontpath-simple-renderer
* output in animated Gif? as a module
### Referencing earlier states
Complex sequences with masking could require accessing previous states (or nonlinearity):
* flood-fill an area
* select only the flooded area
* roundabout: lighten everything to <50%, then flood-fill with black? Not 100% reliable.
* roundabout 2: `flood fill`, then `blink-diff` with original
* then add step which recovers original image, repeat `flood-fill`/`blink-diff` for second region
* reference above masked states in a `mask` module, with `maskModule.draw(image, { getMask: function() { return maskImg } })`
****
**Notes:**
`pattern-fill` module to use patterns in JS canvas:
```js
var c=document.getElementById("myCanvas");
var ctx=c.getContext("2d");
var img=document.getElementById("lamp");
var pat=ctx.createPattern(img,"repeat");
ctx.rect(0,0,150,100);
ctx.fillStyle=pat;
ctx.fill();
```
Masking:
```js
ctx.save();
ctx.beginPath();
ctx.moveTo(0, 0);
ctx.lineTo(160, 600);
ctx.rect(0, 0, 160, 600);
ctx.closePath();
ctx.clip();
ctx.drawImage(img, 0, 0);
ctx.restore();
```
****
## UI notes:
* visual nodes-and-lines UI: https://github.com/flowhub/the-graph
* https://flowhub.github.io/the-graph/examples/demo-simple.html
```js
settings: {
'threshold': {
type: 'slider',
label: 'Threshold',
default: 50,
min: 0,
max: 100
},
'colors': {
type: 'select',
label: 'Colors',
options: [
{ name: '0', value: '0', default: true },
{ name: '1', value: '1' },
{ name: '2', value: '2' }
]
}
}
```
Possible web-based commandline interface: https://hyper.is/?
### Path cutting
* threshold
* vectorize
* edge detect
* direction find (vectorize and colorize)

File diff suppressed because one or more lines are too long

BIN
examples/red.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 277 B

BIN
examples/test.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

1
examples/test.gif.js Normal file

File diff suppressed because one or more lines are too long

1
examples/test.png.js Normal file

File diff suppressed because one or more lines are too long

View File

@@ -1,8 +1,8 @@
{ {
"name": "image-sequencer", "name": "image-sequencer",
"version": "0.0.1", "version": "0.1.0",
"description": "A modular JavaScript image manipulation library modeled on a storyboard.", "description": "A modular JavaScript image manipulation library modeled on a storyboard.",
"main": "dist/image-sequencer.js", "main": "src/ImageSequencer.js",
"scripts": { "scripts": {
"test": "tape test/*.js | tap-spec; browserify test/image-sequencer.js test/chain.js | tape-run --render=\"tap-spec\"" "test": "tape test/*.js | tap-spec; browserify test/image-sequencer.js test/chain.js | tape-run --render=\"tap-spec\""
}, },
@@ -26,7 +26,6 @@
"urify": "^2.1.0" "urify": "^2.1.0"
}, },
"devDependencies": { "devDependencies": {
"base64-stream": "~0.1.3",
"browserify": "13.0.0", "browserify": "13.0.0",
"buffer": "~5.0.2", "buffer": "~5.0.2",
"get-pixels": "~3.3.0", "get-pixels": "~3.3.0",

View File

@@ -1,31 +1,22 @@
function AddStep(ref, image, name, o) { function AddStep(ref, image, name, o) {
function addStep(image, name, o_) { function addStep(image, name, o_) {
ref.log('\x1b[36m%s\x1b[0m','adding step \"' + name + '\" to \"' + image + '\".'); var o = ref.copy(o_);
o.number = ref.options.sequencerCounter++; //Gives a Unique ID to each step
o = ref.copy(o_);
o.id = ref.options.sequencerCounter++; //Gives a Unique ID to each step
o.name = o_.name || name; o.name = o_.name || name;
o.selector = o_.selector || 'ismod-' + name; o.selector = o_.selector || 'ismod-' + name;
o.container = o_.container || ref.options.selector; o.container = o_.container || ref.options.selector;
o.image = image; o.image = image;
o.inBrowser = ref.options.inBrowser; o.inBrowser = ref.options.inBrowser;
var module = ref.modules[name].call(ref.images,o); var UI = ref.UI({
stepName: o.name,
stepID: o.number,
imageName: o.image
});
var module = ref.modules[name](o,UI);
ref.images[image].steps.push(module); ref.images[image].steps.push(module);
function defaultSetupModule() {
if (ref.options.ui && ref.options.ui!="none") module.options.ui = ref.options.ui({
selector: o.selector,
title: module.options.title,
id: o.id
});
}
if (module.hasOwnProperty('setup')) module.setup(); // add a default UI, unless the module has one specified
else defaultSetupModule.apply(module); // run default setup() in scope of module (is this right?)
// tell the UI that a step has been added.
return true; return true;
} }

View File

@@ -15,7 +15,7 @@ function copy(a) {
if (objTypeOf(a) == "Array") return a.slice(); if (objTypeOf(a) == "Array") return a.slice();
if (objTypeOf(a) == "Object") { if (objTypeOf(a) == "Object") {
var b = {}; var b = {};
for (v in a) { for (var v in a) {
b[v] = copy(a[v]); b[v] = copy(a[v]);
} }
return b; return b;
@@ -25,11 +25,11 @@ function copy(a) {
function formatInput(args,format,images) { function formatInput(args,format,images) {
images = []; images = [];
for (image in this.images) { for (var image in this.images) {
images.push(image); images.push(image);
} }
json_q = {}; var json_q = {};
format_i = format; var format_i = format;
if (format == "+") if (format == "+")
format = ['o_string_a', 'string_a', 'o_object']; format = ['o_string_a', 'string_a', 'o_object'];
else if (format == "-") else if (format == "-")
@@ -76,8 +76,8 @@ function formatInput(args,format,images) {
if(format[0] == "o_string_a") { if(format[0] == "o_string_a") {
if(args.length == format.length - 1) { if(args.length == format.length - 1) {
insert = false; var insert = false;
for (i in args) { for (var i in args) {
if (format[parseInt(i)+1].includes( typeof(getPrimitive(args[i])) )){ if (format[parseInt(i)+1].includes( typeof(getPrimitive(args[i])) )){
insert = true; insert = true;
} }
@@ -89,15 +89,15 @@ function formatInput(args,format,images) {
} }
else if (format[0] == "o_string" && format_i == "l" && args.length == 2) { else if (format[0] == "o_string" && format_i == "l" && args.length == 2) {
if (typeof(args[0]) == "string") { if (typeof(args[0]) == "string") {
identifier = "image"; var identifier = "image";
number = 1; var number = 1;
while (this.images.hasOwnProperty(identifier+number)) number++; while (this.images.hasOwnProperty(identifier+number)) number++;
args.splice(0,0,identifier+number); args.splice(0,0,identifier+number);
} }
} }
if(args.length == format.length) { if(args.length == format.length) {
for (i in format) { for (var i in format) {
if (format[i].substr(format[i].length-2,2)=="_a") if (format[i].substr(format[i].length-2,2)=="_a")
args[i] = makeArray(args[i]); args[i] = makeArray(args[i]);
} }
@@ -106,12 +106,12 @@ function formatInput(args,format,images) {
if (args.length == 1) { if (args.length == 1) {
json_q = copy(args[0]); json_q = copy(args[0]);
if(!(format_i == "r" || format_i == "l")) { if(!(format_i == "r" || format_i == "l")) {
for (img in json_q) for (var img in json_q)
json_q[img] = makeArray(json_q[img]); json_q[img] = makeArray(json_q[img]);
} }
} }
else if (format_i == "r") { else if (format_i == "r") {
for (img in args[0]) json_q[args[0][img]] = args[1]; for (var img in args[0]) json_q[args[0][img]] = args[1];
} }
else if (format_i == "l") { else if (format_i == "l") {
json_q = { json_q = {
@@ -121,12 +121,12 @@ function formatInput(args,format,images) {
json_q.images[args[0]] = args[1]; json_q.images[args[0]] = args[1];
} }
else { else {
for (img in args[0]) { for (var img in args[0]) {
image = args[0][img]; var image = args[0][img];
json_q[image] = []; json_q[image] = [];
if(format_i == "+") { if(format_i == "+") {
for(s in args[1]) { for(var s in args[1]) {
json_q[image].push({ json_q[image].push({
name: args[1][s], name: args[1][s],
o: args[2] o: args[2]
@@ -139,8 +139,8 @@ function formatInput(args,format,images) {
} }
if(format_i == "^") { if(format_i == "^") {
size = this.images[image].steps.length; var size = this.images[image].steps.length;
index = args[1]; var index = args[1];
index = (index==size)?index:index%size; index = (index==size)?index:index%size;
if (index<0) index += size+1; if (index<0) index += size+1;
json_q[image].push({ json_q[image].push({
@@ -155,7 +155,7 @@ function formatInput(args,format,images) {
if(format_i == "l") { if(format_i == "l") {
json_q.loadedimages = []; json_q.loadedimages = [];
for (i in json_q.images) json_q.loadedimages.push(i); for (var i in json_q.images) json_q.loadedimages.push(i);
} }
return json_q; return json_q;

View File

@@ -24,7 +24,7 @@ ImageSequencer = function ImageSequencer(options) {
if (objTypeOf(a) == "Array") return a.slice(); if (objTypeOf(a) == "Array") return a.slice();
if (objTypeOf(a) == "Object") { if (objTypeOf(a) == "Object") {
var b = {}; var b = {};
for (v in a) { for (var v in a) {
b[v] = copy(a[v]); b[v] = copy(a[v]);
} }
return b; return b;
@@ -36,13 +36,15 @@ ImageSequencer = function ImageSequencer(options) {
return (objTypeOf(input)=="Array")?input:[input]; return (objTypeOf(input)=="Array")?input:[input];
} }
formatInput = require('./FormatInput');
var image, var image,
steps = [], steps = [],
modules = require('./Modules'), modules = require('./Modules'),
formatInput = require('./FormatInput'),
images = {}, images = {},
inputlog = []; inputlog = [],
UI;
setUI();
// if in browser, prompt for an image // if in browser, prompt for an image
// if (options.imageSelect || options.inBrowser) addStep('image-select'); // if (options.imageSelect || options.inBrowser) addStep('image-select');
@@ -50,15 +52,15 @@ ImageSequencer = function ImageSequencer(options) {
function addSteps(){ function addSteps(){
const this_ = (this.name == "ImageSequencer")?this:this.sequencer; const this_ = (this.name == "ImageSequencer")?this:this.sequencer;
args = (this.name == "ImageSequencer")?[]:[this.images]; var args = (this.name == "ImageSequencer")?[]:[this.images];
json_q = {}; var json_q = {};
for(arg in arguments){args.push(copy(arguments[arg]));} for(var arg in arguments){args.push(copy(arguments[arg]));}
json_q = formatInput.call(this_,args,"+"); json_q = formatInput.call(this_,args,"+");
inputlog.push({method:"addSteps", json_q:copy(json_q)}); inputlog.push({method:"addSteps", json_q:copy(json_q)});
for (i in json_q) for (var i in json_q)
for (j in json_q[i]) for (var j in json_q[i])
require("./AddStep")(this_,i,json_q[i][j].name,json_q[i][j].o); require("./AddStep")(this_,i,json_q[i][j].name,json_q[i][j].o);
return this; return this;
@@ -67,25 +69,25 @@ ImageSequencer = function ImageSequencer(options) {
function removeStep(image,index) { function removeStep(image,index) {
//remove the step from images[image].steps and redraw remaining images //remove the step from images[image].steps and redraw remaining images
if(index>0) { if(index>0) {
log('\x1b[31m%s\x1b[0m',"Removing "+index+" from "+image); images[image].steps[index].UI.onRemove();
images[image].steps.splice(index,1); images[image].steps.splice(index,1);
} }
//tell the UI a step has been removed //tell the UI a step has been removed
} }
function removeSteps(image,index) { function removeSteps(image,index) {
run = {}; var run = {}, indices;
const this_ = (this.name == "ImageSequencer")?this:this.sequencer; const this_ = (this.name == "ImageSequencer")?this:this.sequencer;
args = (this.name == "ImageSequencer")?[]:[this.images]; var args = (this.name == "ImageSequencer")?[]:[this.images];
for(arg in arguments) args.push(copy(arguments[arg])); for(var arg in arguments) args.push(copy(arguments[arg]));
json_q = formatInput.call(this_,args,"-"); var json_q = formatInput.call(this_,args,"-");
inputlog.push({method:"removeSteps", json_q:copy(json_q)}); inputlog.push({method:"removeSteps", json_q:copy(json_q)});
for (img in json_q) { for (var img in json_q) {
indices = json_q[img].sort(function(a,b){return b-a}); indices = json_q[img].sort(function(a,b){return b-a});
run[img] = indices[indices.length-1]; run[img] = indices[indices.length-1];
for (i in indices) for (var i in indices)
removeStep(img,indices[i]); removeStep(img,indices[i]);
} }
// this.run(run); // This is creating problems // this.run(run); // This is creating problems
@@ -93,18 +95,18 @@ ImageSequencer = function ImageSequencer(options) {
} }
function insertSteps(image, index, name, o) { function insertSteps(image, index, name, o) {
run = {}; var run = {};
const this_ = (this.name == "ImageSequencer")?this:this.sequencer; const this_ = (this.name == "ImageSequencer")?this:this.sequencer;
args = (this.name == "ImageSequencer")?[]:[this.images]; var args = (this.name == "ImageSequencer")?[]:[this.images];
for (arg in arguments) args.push(arguments[arg]); for (var arg in arguments) args.push(arguments[arg]);
json_q = formatInput.call(this_,args,"^"); var json_q = formatInput.call(this_,args,"^");
inputlog.push({method:"insertSteps", json_q:copy(json_q)}); inputlog.push({method:"insertSteps", json_q:copy(json_q)});
for (img in json_q) { for (var img in json_q) {
var details = json_q[img]; var details = json_q[img];
details = details.sort(function(a,b){return b.index-a.index}); details = details.sort(function(a,b){return b.index-a.index});
for (i in details) for (var i in details)
require("./InsertStep")(this_,img,details[i].index,details[i].name,details[i].o); require("./InsertStep")(this_,img,details[i].index,details[i].name,details[i].o);
run[img] = details[details.length-1].index; run[img] = details[details.length-1].index;
} }
@@ -113,17 +115,16 @@ ImageSequencer = function ImageSequencer(options) {
} }
function run(t_image,t_from) { function run(t_image,t_from) {
log('\x1b[32m%s\x1b[0m',"Running the Sequencer!");
const this_ = (this.name == "ImageSequencer")?this:this.sequencer; const this_ = (this.name == "ImageSequencer")?this:this.sequencer;
args = (this.name == "ImageSequencer")?[]:[this.images]; var args = (this.name == "ImageSequencer")?[]:[this.images];
for (var arg in arguments) args.push(copy(arguments[arg])); for (var arg in arguments) args.push(copy(arguments[arg]));
callback = function() {}; var callback = function() {};
for (var arg in args) for (var arg in args)
if(objTypeOf(args[arg]) == "Function") if(objTypeOf(args[arg]) == "Function")
callback = args.splice(arg,1)[0]; callback = args.splice(arg,1)[0];
json_q = formatInput.call(this_,args,"r"); var json_q = formatInput.call(this_,args,"r");
require('./Run')(this_, json_q, callback); require('./Run')(this_, json_q, callback);
@@ -131,26 +132,39 @@ ImageSequencer = function ImageSequencer(options) {
} }
function loadImages() { function loadImages() {
args = []; var args = [];
for (arg in arguments) args.push(copy(arguments[arg])); var sequencer = this;
json_q = formatInput.call(this,args,"l"); for (var arg in arguments) args.push(copy(arguments[arg]));
var json_q = formatInput.call(this,args,"l");
inputlog.push({method:"loadImages", json_q:copy(json_q)}); inputlog.push({method:"loadImages", json_q:copy(json_q)});
loadedimages = this.copy(json_q.loadedimages); var loadedimages = this.copy(json_q.loadedimages);
// require('./LoadImage')(this,i,json_q.images[i]);
for (i in json_q.images) var ret = {
require('./LoadImage')(this,i,json_q.images[i])
json_q.callback();
return {
name: "ImageSequencer Wrapper", name: "ImageSequencer Wrapper",
sequencer: this, sequencer: this,
addSteps: this.addSteps, addSteps: this.addSteps,
removeSteps: this.removeSteps, removeSteps: this.removeSteps,
insertSteps: this.insertSteps, insertSteps: this.insertSteps,
run: this.run, run: this.run,
UI: this.UI,
setUI: this.setUI,
images: loadedimages images: loadedimages
}; };
function load(i) {
if(i==loadedimages.length) {
json_q.callback.call(ret);
return;
}
var img = loadedimages[i];
require('./LoadImage')(sequencer,img,json_q.images[img],function(){
load(++i);
});
}
load(0);
} }
function replaceImage(selector,steps,options) { function replaceImage(selector,steps,options) {
@@ -158,9 +172,21 @@ ImageSequencer = function ImageSequencer(options) {
return require('./ReplaceImage')(this,selector,steps); return require('./ReplaceImage')(this,selector,steps);
} }
function setUI(_UI) {
UI = require('./UserInterface')(_UI,options);
return UI;
}
return { return {
//literals and objects
name: "ImageSequencer", name: "ImageSequencer",
options: options, options: options,
inputlog: inputlog,
modules: modules,
images: images,
UI: UI,
//user functions
loadImages: loadImages, loadImages: loadImages,
loadImage: loadImages, loadImage: loadImages,
addSteps: addSteps, addSteps: addSteps,
@@ -168,10 +194,9 @@ ImageSequencer = function ImageSequencer(options) {
insertSteps: insertSteps, insertSteps: insertSteps,
replaceImage: replaceImage, replaceImage: replaceImage,
run: run, run: run,
inputlog: inputlog, setUI: setUI,
modules: modules,
images: images, //other functions
ui: options.ui,
log: log, log: log,
objTypeOf: objTypeOf, objTypeOf: objTypeOf,
copy: copy copy: copy

View File

@@ -1,31 +1,22 @@
function InsertStep(ref, image, index, name, o) { function InsertStep(ref, image, index, name, o) {
function insertStep(image, index, name, o_) { function insertStep(image, index, name, o_) {
ref.log('\x1b[36m%s\x1b[0m','inserting step \"' + name + '\" to \"' + image + '\" at \"'+index+'\".'); var o = ref.copy(o_);
o.number = ref.options.sequencerCounter++; //Gives a Unique ID to each step
o = ref.copy(o_); o.name = o_.name || name;
o.id = ref.options.sequencerCounter++; //Gives a Unique ID to each step o.selector = o_.selector || 'ismod-' + name;
o.name = o.name || name; o.container = o_.container || ref.options.selector;
o.selector = o.selector || 'ismod-' + name;
o.container = o.container || ref.options.selector;
o.image = image; o.image = image;
if(index==-1) index = ref.images[image].steps.length; if(index==-1) index = ref.images[image].steps.length;
var module = ref.modules[name](o); var UI = ref.UI({
ref.images[image].steps.splice(index, 0, module); stepName: o.name,
stepID: o.number,
function defaultSetupModule() { imageName: o.image
if (ref.options.ui && ref.options.ui!="none") module.options.ui = ref.options.ui({ });
selector: o.selector, var module = ref.modules[name](o,UI);
title: module.options.title, ref.images[image].steps.splice(index,0,module);
id: o.id
});
}
if (module.hasOwnProperty('setup')) module.setup(); // add a default UI, unless the module has one specified
else defaultSetupModule.apply(module); // run default setup() in scope of module (is this right?)
// tell the UI that a step has been inserted.
return true; return true;
} }

View File

@@ -1,15 +1,50 @@
function LoadImage(ref, name, src) { function LoadImage(ref, name, src, main_callback) {
function CImage(src) { function makeImage(datauri) {
datauri = (ref.options.inBrowser || src.substring(0,11) == "data:image/")?(src):require('urify')(src); var image = {
image = {
src: datauri, src: datauri,
format: datauri.split(':')[1].split(';')[0].split('/')[1] format: datauri.split(':')[1].split(';')[0].split('/')[1]
} }
return image; return image;
} }
function CImage(src, callback) {
var datauri;
if (!!src.match(/^data:/i)) {
datauri = src;
callback(datauri);
}
else if (!ref.options.inBrowser && !!src.match(/^https?:\/\//i)) {
require( src.match(/^(https?):\/\//i)[1] ).get(src,function(res){
var data = '';
var contentType = res.headers['content-type'];
res.setEncoding('base64');
res.on('data',function(chunk) {data += chunk;});
res.on('end',function() {
callback("data:"+contentType+";base64,"+data);
});
});
}
else if (ref.options.inBrowser) {
var ext = src.split('.').pop();
var image = document.createElement('img');
var canvas = document.createElement('canvas');
var context = canvas.getContext('2d');
image.onload = function() {
canvas.width = image.naturalWidth;
canvas.height = image.naturalHeight;
context.drawImage(image,0,0);
datauri = canvas.toDataURL(ext);
callback(datauri);
}
image.src = src;
}
else {
datauri = require('urify')(src);
callback(datauri);
}
}
function loadImage(name, src) { function loadImage(name, src) {
image = { var image = {
src: src, src: src,
steps: [{ steps: [{
options: { options: {
@@ -17,6 +52,11 @@ function LoadImage(ref, name, src) {
name: "load-image", name: "load-image",
title: "Load Image" title: "Load Image"
}, },
UI: ref.UI({
stepName: "load-image",
stepID: ref.options.sequencerCounter++,
imageName: name
}),
draw: function() { draw: function() {
if(arguments.length==1){ if(arguments.length==1){
this.output = CImage(arguments[0]); this.output = CImage(arguments[0]);
@@ -29,10 +69,18 @@ function LoadImage(ref, name, src) {
} }
return false; return false;
}, },
output: CImage(src)
}] }]
}; };
ref.images[name] = image; CImage(src, function(datauri) {
var output = makeImage(datauri);
image.steps[0].output = output;
ref.images[name] = image;
ref.images[name].steps[0].UI.onSetup();
ref.images[name].steps[0].UI.onDraw();
ref.images[name].steps[0].UI.onComplete(image.steps[0].output.src);
main_callback();
return true;
});
} }
return loadImage(name,src); return loadImage(name,src);

View File

@@ -1,12 +1,12 @@
function ReplaceImage(ref,selector,steps,options) { function ReplaceImage(ref,selector,steps,options) {
if(!ref.options.inBrowser) return false; // This isn't for Node.js if(!ref.options.inBrowser) return false; // This isn't for Node.js
this_ = ref; var this_ = ref;
var input = document.querySelectorAll(selector); var input = document.querySelectorAll(selector);
var images = []; var images = [];
for (i = 0; i < input.length; i++) for (var i = 0; i < input.length; i++)
if (input[i] instanceof HTMLImageElement) images.push(input[i]); if (input[i] instanceof HTMLImageElement) images.push(input[i]);
for (i in images) { for (var i in images) {
the_image = images[i]; var the_image = images[i];
var url = images[i].src; var url = images[i].src;
var ext = url.split('.').pop(); var ext = url.split('.').pop();

View File

@@ -1,47 +1,47 @@
function Run(ref, json_q, callback) { function Run(ref, json_q, callback) {
function drawStep(drawarray,pos) { function drawStep(drawarray,pos) {
if(pos==drawarray.length) { if(pos==drawarray.length) {
image = drawarray[pos-1].image; var image = drawarray[pos-1].image;
if(ref.objTypeOf(callback)=='Function'){ if(ref.objTypeOf(callback)=='Function'){
steps = ref.images[image].steps; var steps = ref.images[image].steps;
out = steps[steps.length-1].output.src; var out = steps[steps.length-1].output.src;
callback(out); callback(out);
return true; return true;
} }
} }
image = drawarray[pos].image; var image = drawarray[pos].image;
i = drawarray[pos].i; var i = drawarray[pos].i;
input = ref.images[image].steps[i-1].output; var input = ref.images[image].steps[i-1].output;
ref.images[image].steps[i].draw(ref.copy(input),function(){ ref.images[image].steps[i].draw(ref.copy(input),function(){
drawStep(drawarray,++pos); drawStep(drawarray,++pos);
}); });
} }
function drawSteps(json_q) { function drawSteps(json_q) {
drawarray = []; var drawarray = [];
for (image in json_q) { for (var image in json_q) {
no_steps = ref.images[image].steps.length; var no_steps = ref.images[image].steps.length;
init = json_q[image]; var init = json_q[image];
for(i = 0; i < no_steps-init; i++) { for(var i = 0; i < no_steps-init; i++) {
drawarray.push({image: image,i: init+i}); drawarray.push({image: image,i: init+i});
} }
} }
drawStep(drawarray,0); drawStep(drawarray,0);
} }
function filter(json_q){ function filter(json_q){
for (image in json_q) { for (var image in json_q) {
if (json_q[image]==0 && ref.images[image].steps.length==1) if (json_q[image]==0 && ref.images[image].steps.length==1)
delete json_q[image]; delete json_q[image];
else if (json_q[image]==0) json_q[image]++; else if (json_q[image]==0) json_q[image]++;
} }
for (image in json_q) { for (var image in json_q) {
prevstep = ref.images[image].steps[json_q[image]-1]; var prevstep = ref.images[image].steps[json_q[image]-1];
while (typeof(prevstep) == "undefined" || typeof(prevstep.output) == "undefined") { while (typeof(prevstep) == "undefined" || typeof(prevstep.output) == "undefined") {
prevstep = ref.images[image].steps[(--json_q[image]) - 1]; prevstep = ref.images[image].steps[(--json_q[image]) - 1];
} }
} }
return json_q; return json_q;
} }
json_q = filter(json_q); var json_q = filter(json_q);
return drawSteps(json_q); return drawSteps(json_q);
} }
module.exports = Run; module.exports = Run;

View File

@@ -1,41 +1,72 @@
/* /*
* Default UI for each image-sequencer module * Default UI for each image-sequencer module
*/ */
module.exports = function UserInterface(options) { module.exports = function UserInterface(UI,options) {
options = options || {}; return function userInterface(identity) {
options.container = options.container || ".panels";
options.id = options.id;
options.instanceName = options.instanceName;
options.random = options.random || parseInt(Math.random() * (new Date()).getTime() / 1000000);
options.uniqueSelector = options.uniqueSelector || options.selector + '-' + options.random;
$(options.container).append('<div class="panel ' + options.selector + ' ' + options.uniqueSelector + '" id="sequencer-'+options.id+'"><div class="image"></div></div>');
options.el = options.el || $('.' + options.uniqueSelector);
createLabel(options.el);
// method to remove the UI for a given method, and remove the step var UI = UI || {};
function display(image) {
options.el.find('.image').html(image);
}
// method to remove the UI for a given method, and remove the step UI.onSetup = UI.onSetup || function() {
function remove() { if(options.ui == false) {
$('div#sequencer-'+options.id).remove(); // No UI
} }
else if(options.inBrowser) {
// Create and append an HTML Element
console.log("Added Step \""+identity.stepName+"\" to \""+identity.imageName+"\".");
}
else {
// Create a NodeJS Object
console.log('\x1b[36m%s\x1b[0m',"Added Step \""+identity.stepName+"\" to \""+identity.imageName+"\".");
// method to reorder steps, and update the UI }
//function move() {} }
function createLabel(el) { UI.onDraw = UI.onDraw || function() {
if (options.title) el.prepend('<h3 class="title">' + options.title + '</h3> <button class="btn btn-default" onclick="'+options.instanceName+'.removeStep('+options.id+')">Remove Step</button>'); if (options.ui == false) {
} // No UI
}
else if(options.inBrowser) {
// Overlay a loading spinner
console.log("Drawing Step \""+identity.stepName+"\" on \""+identity.imageName+"\".");
}
else {
// Don't do anything
console.log('\x1b[33m%s\x1b[0m',"Drawing Step \""+identity.stepName+"\" on \""+identity.imageName+"\".");
}
}
UI.onComplete = UI.onComplete || function(output) {
if (options.ui == false) {
// No UI
}
else if(options.inBrowser) {
// Update the DIV Element
// Hide the laoding spinner
console.log("Drawn Step \""+identity.stepName+"\" on \""+identity.imageName+"\".");
}
else {
// Update the NodeJS Object
console.log('\x1b[32m%s\x1b[0m',"Drawn Step \""+identity.stepName+"\" on \""+identity.imageName+"\".");
}
}
UI.onRemove = UI.onRemove || function(callback) {
if(options.ui == false){
// No UI
}
else if(options.inBrowser) {
// Remove the DIV Element
console.log("Removing Step \""+identity.stepName+"\" of \""+identity.imageName+"\".");
}
else {
// Delete the NodeJS Object
console.log('\x1b[31m%s\x1b[0m',"Removing Step \""+identity.stepName+"\" of \""+identity.imageName+"\".");
}
}
return UI;
return {
el: options.el,
uniqueSelector: options.uniqueSelector,
selector: options.selector,
display: display,
remove: remove
} }
} }

View File

@@ -1,17 +1,15 @@
module.exports = function Crop(input,options,callback) { module.exports = function Crop(input,options,callback) {
var getPixels = require("get-pixels"), var getPixels = require('get-pixels'),
savePixels = require("save-pixels"), savePixels = require('save-pixels');
base64 = require('base64-stream');
getPixels(input.src,function(err,pixels){ getPixels(input.src,function(err,pixels){
var newdata = [];
var ox = options.x || 0; var ox = options.x || 0;
var oy = options.y || 0; var oy = options.y || 0;
var w = options.w || Math.floor(0.5*pixels.shape[0]); var w = options.w || Math.floor(0.5*pixels.shape[0]);
var h = options.h || Math.floor(0.5*pixels.shape[1]); var h = options.h || Math.floor(0.5*pixels.shape[1]);
var iw = pixels.shape[0]; //Width of Original Image var iw = pixels.shape[0]; //Width of Original Image
newarray = new Uint8Array(4*w*h); var newarray = new Uint8Array(4*w*h);
for (var n = oy; n < oy + h; n++) { for (var n = oy; n < oy + h; n++) {
newarray.set(pixels.data.slice(n*4*iw + ox, n*4*iw + ox + 4*w),4*w*(n-oy)); newarray.set(pixels.data.slice(n*4*iw + ox, n*4*iw + ox + 4*w),4*w*(n-oy));
} }
@@ -19,15 +17,19 @@ module.exports = function Crop(input,options,callback) {
pixels.shape = [w,h,4]; pixels.shape = [w,h,4];
pixels.stride[1] = 4*w; pixels.stride[1] = 4*w;
options.format = "jpeg"; var chunks = [];
var totalLength = 0;
w = base64.encode();
var r = savePixels(pixels, options.format); var r = savePixels(pixels, options.format);
r.pipe(w).on('finish',function(){
data = w.read().toString(); r.on('data', function(chunk){
datauri = 'data:image/' + options.format + ';base64,' + data; 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;
callback(datauri,options.format); callback(datauri,options.format);
}); });
}); });
};
}

View File

@@ -13,21 +13,23 @@
* y = options.y * y = options.y
* y = options.y + options.h * y = options.y + options.h
*/ */
module.exports = function CropModule(options) { module.exports = function CropModule(options,UI) {
options = options || {}; options = options || {};
options.title = "Crop Image"; options.title = "Crop Image";
this_ = this; UI.onSetup();
var output var output
function draw(input,callback) { function draw(input,callback) {
const this_ = this; UI.onDraw();
const step = this;
require('./Crop')(input,options,function(out,format){ require('./Crop')(input,options,function(out,format){
this_.output = { step.output = {
src: out, src: out,
format: format format: format
} }
UI.onComplete(out);
callback(); callback();
}); });
@@ -37,6 +39,7 @@
return { return {
options: options, options: options,
draw: draw, draw: draw,
output: output output: output,
UI: UI
} }
} }

View File

@@ -1,20 +1,23 @@
/* /*
* Demo Module. Does nothing. Adds a step where output is equal to input. * Demo Module. Does nothing. Adds a step where output is equal to input.
*/ */
module.exports = function DoNothing(options) { module.exports = function DoNothing(options,UI) {
options = options || {}; options = options || {};
options.title = "Do Nothing"; options.title = "Do Nothing";
this_ = this; UI.onSetup();
var output var output;
function draw(input,callback) { function draw(input,callback) {
UI.onDraw();
this.output = input; this.output = input;
callback(); callback();
UI.onComplete(this.output.src);
} }
return { return {
options: options, options: options,
draw: draw, draw: draw,
output: output output: output,
UI: UI
} }
} }

View File

@@ -1,19 +1,24 @@
/* /*
* This module extracts pixels and saves them as it is. * This module extracts pixels and saves them as it is.
*/ */
module.exports = function DoNothingPix(options) { module.exports = function DoNothingPix(options,UI) {
options = options || {}; options = options || {};
options.title = "Do Nothing with pixels"; options.title = "Do Nothing with pixels";
UI.onSetup();
var output; var output;
function draw(input,callback) { function draw(input,callback) {
this_ = this;
UI.onDraw();
const step = this;
function changePixel(r, g, b, a) { function changePixel(r, g, b, a) {
return [r, g, b, a]; return [r, g, b, a];
} }
function output(image,datauri,mimetype){ function output(image,datauri,mimetype){
this_.output = {src:datauri,format:mimetype} step.output = {src:datauri,format:mimetype}
UI.onComplete(datauri);
} }
return require('../_nomodule/PixelManipulation.js')(input, { return require('../_nomodule/PixelManipulation.js')(input, {
output: output, output: output,
@@ -22,11 +27,13 @@ module.exports = function DoNothingPix(options) {
image: options.image, image: options.image,
callback: callback callback: callback
}); });
} }
return { return {
options: options, options: options,
draw: draw, draw: draw,
output: output output: output,
UI: UI
} }
} }

View File

@@ -1,22 +1,25 @@
/* /*
* Display only the green channel * Display only the green channel
*/ */
module.exports = function GreenChannel(options) { module.exports = function GreenChannel(options,UI) {
options = options || {}; options = options || {};
options.title = "Green channel only"; options.title = "Green channel only";
options.description = "Displays only the green channel of an image"; options.description = "Displays only the green channel of an image";
UI.onSetup();
var output; var output;
//function setup() {} // optional
function draw(input,callback) { function draw(input,callback) {
this_ = this;
UI.onDraw();
const step = this;
function changePixel(r, g, b, a) { function changePixel(r, g, b, a) {
return [0, g, 0, a]; return [0, g, 0, a];
} }
function output(image,datauri,mimetype){ function output(image,datauri,mimetype){
this_.output = {src:datauri,format:mimetype} step.output = {src:datauri,format:mimetype};
UI.onComplete(datauri);
} }
return require('../_nomodule/PixelManipulation.js')(input, { return require('../_nomodule/PixelManipulation.js')(input, {
output: output, output: output,
@@ -25,12 +28,14 @@ module.exports = function GreenChannel(options) {
image: options.image, image: options.image,
callback: callback callback: callback
}); });
} }
return { return {
options: options, options: options,
//setup: setup, // optional //setup: setup, // optional
draw: draw, draw: draw,
output: output output: output,
UI: UI
} }
} }

View File

@@ -1,22 +1,27 @@
/* /*
* Display only the green channel * Display only the green channel
*/ */
module.exports = function GreenChannel(options) { module.exports = function GreenChannel(options,UI) {
options = options || {}; options = options || {};
options.title = "Invert Colors"; options.title = "Invert Colors";
options.description = "Inverts the colors of the image"; options.description = "Inverts the colors of the image";
UI.onSetup();
var output; var output;
//function setup() {} // optional //function setup() {} // optional
function draw(input,callback) { function draw(input,callback) {
this_ = this;
UI.onDraw();
const step = this;
function changePixel(r, g, b, a) { function changePixel(r, g, b, a) {
return [255-r, 255-g, 255-b, a]; return [255-r, 255-g, 255-b, a];
} }
function output(image,datauri,mimetype){ function output(image,datauri,mimetype){
this_.output = {src:datauri,format:mimetype} step.output = {src:datauri,format:mimetype};
UI.onComplete(datauri);
} }
return require('../_nomodule/PixelManipulation.js')(input, { return require('../_nomodule/PixelManipulation.js')(input, {
output: output, output: output,
@@ -25,12 +30,14 @@ module.exports = function GreenChannel(options) {
image: options.image, image: options.image,
callback: callback callback: callback
}); });
} }
return { return {
options: options, options: options,
//setup: setup, // optional //setup: setup, // optional
draw: draw, draw: draw,
output: output output: output,
UI: UI
} }
} }

View File

@@ -1,21 +1,26 @@
/* /*
* NDVI with red filter (blue channel is infrared) * NDVI with red filter (blue channel is infrared)
*/ */
module.exports = function NdviRed(options) { module.exports = function NdviRed(options,UI) {
options = options || {}; options = options || {};
options.title = "NDVI for red-filtered cameras (blue is infrared)"; options.title = "NDVI for red-filtered cameras (blue is infrared)";
UI.onSetup();
var output; var output;
function draw(input,callback) { function draw(input,callback) {
this_ = this;
UI.onDraw();
const step = this;
function changePixel(r, g, b, a) { function changePixel(r, g, b, a) {
var ndvi = (b - r) / (1.00 * b + r); var ndvi = (b - r) / (1.00 * b + r);
var x = 255 * (ndvi + 1) / 2; var x = 255 * (ndvi + 1) / 2;
return [x, x, x, a]; return [x, x, x, a];
} }
function output(image,datauri,mimetype){ function output(image,datauri,mimetype){
this_.output = {src:datauri,format:mimetype} step.output = {src:datauri,format:mimetype};
UI.onComplete(datauri);
} }
return require('../_nomodule/PixelManipulation.js')(input, { return require('../_nomodule/PixelManipulation.js')(input, {
output: output, output: output,
@@ -24,10 +29,13 @@ module.exports = function NdviRed(options) {
image: options.image, image: options.image,
callback: callback callback: callback
}); });
} }
return { return {
options: options, options: options,
draw: draw draw: draw,
output: output,
UI:UI
} }
} }

View File

@@ -1,11 +1,15 @@
module.exports = function SegmentedColormap(options) { module.exports = function SegmentedColormap(options,UI) {
options = options || {}; options = options || {};
options.title = "Segmented Colormap"; options.title = "Segmented Colormap";
UI.onSetup();
var output; var output;
function draw(input,callback) { function draw(input,callback) {
this_ = this;
UI.onDraw();
const step = this;
function changePixel(r, g, b, a) { function changePixel(r, g, b, a) {
var ndvi = (b - r) / (r + b); var ndvi = (b - r) / (r + b);
var normalized = (ndvi + 1) / 2; var normalized = (ndvi + 1) / 2;
@@ -13,7 +17,8 @@ module.exports = function SegmentedColormap(options) {
return [res[0], res[1], res[2], 255]; return [res[0], res[1], res[2], 255];
} }
function output(image,datauri,mimetype){ function output(image,datauri,mimetype){
this_.output = {src:datauri,format:mimetype} step.output = {src:datauri,format:mimetype};
UI.onComplete(datauri);
} }
return require('../_nomodule/PixelManipulation.js')(input, { return require('../_nomodule/PixelManipulation.js')(input, {
output: output, output: output,
@@ -22,11 +27,13 @@ module.exports = function SegmentedColormap(options) {
image: options.image, image: options.image,
callback: callback callback: callback
}); });
} }
return { return {
options: options, options: options,
draw: draw, draw: draw,
output: output output: output,
UI: UI
} }
} }

View File

@@ -7,16 +7,15 @@ module.exports = function PixelManipulation(image, options) {
options = options || {}; options = options || {};
options.changePixel = options.changePixel || function changePixel(r, g, b, a) { options.changePixel = options.changePixel || function changePixel(r, g, b, a) {
return [r, g, b, a]; return [r, g, b, a];
} };
var getPixels = require("get-pixels"), var getPixels = require('get-pixels'),
savePixels = require("save-pixels"), savePixels = require('save-pixels');
base64 = require('base64-stream');
getPixels(image.src, function(err, pixels) { getPixels(image.src, function(err, pixels) {
if(err) { if(err) {
console.log("Bad image path") console.log('Bad image path');
return return;
} }
// iterate through pixels; // iterate through pixels;
@@ -25,7 +24,7 @@ module.exports = function PixelManipulation(image, options) {
for(var x = 0; x < pixels.shape[0]; x++) { for(var x = 0; x < pixels.shape[0]; x++) {
for(var y = 0; y < pixels.shape[1]; y++) { for(var y = 0; y < pixels.shape[1]; y++) {
pixel = options.changePixel( var pixel = options.changePixel(
pixels.get(x, y, 0), pixels.get(x, y, 0),
pixels.get(x, y, 1), pixels.get(x, y, 1),
pixels.get(x, y, 2), pixels.get(x, y, 2),
@@ -40,19 +39,22 @@ module.exports = function PixelManipulation(image, options) {
} }
} }
options.format = "jpeg";
// there may be a more efficient means to encode an image object, // there may be a more efficient means to encode an image object,
// but node modules and their documentation are essentially arcane on this point // but node modules and their documentation are essentially arcane on this point
w = base64.encode(); var chunks = [];
var r = savePixels(pixels, options.format); var totalLength = 0;
r.pipe(w).on('finish',function(){ var r = savePixels(pixels, options.format, {quality: 100});
data = w.read().toString();
datauri = 'data:image/' + options.format + ';base64,' + data; 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.output) options.output(options.image,datauri,options.format);
if (options.callback) options.callback(); if (options.callback) options.callback();
}); });
}); });
};
}

View File

@@ -8,7 +8,7 @@ var test = require('tape');
require('../src/ImageSequencer.js'); require('../src/ImageSequencer.js');
var sequencer = ImageSequencer({ ui: "none" }); var sequencer = ImageSequencer({ ui: false });
var red = "data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAYEBQYFBAYGBQYHBwYIChAKCgkJChQODwwQFxQYGBcUFhYaHSUfGhsjHBYWICwgIyYnKSopGR8tMC0oMCUoKSj/2wBDAQcHBwoIChMKChMoGhYaKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCj/wAARCAAQABADASIAAhEBAxEB/8QAFQABAQAAAAAAAAAAAAAAAAAAAAf/xAAUEAEAAAAAAAAAAAAAAAAAAAAA/8QAFQEBAQAAAAAAAAAAAAAAAAAABgj/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwCdABykX//Z"; var red = "data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAYEBQYFBAYGBQYHBwYIChAKCgkJChQODwwQFxQYGBcUFhYaHSUfGhsjHBYWICwgIyYnKSopGR8tMC0oMCUoKSj/2wBDAQcHBwoIChMKChMoGhYaKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCj/wAARCAAQABADASIAAhEBAxEB/8QAFQABAQAAAAAAAAAAAAAAAAAAAAf/xAAUEAEAAAAAAAAAAAAAAAAAAAAA/8QAFQEBAQAAAAAAAAAAAAAAAAAABgj/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwCdABykX//Z";
test('loadImages/loadImage has a name generator.', function (t){ test('loadImages/loadImage has a name generator.', function (t){
@@ -17,22 +17,27 @@ test('loadImages/loadImage has a name generator.', function (t){
t.end(); t.end();
}); });
test('loadImages/loadImage returns a wrapper.', function (t){ test('loadImages/loadImage returns a wrapper in the callback.', function (t){
var returnval = sequencer.loadImage(red); sequencer.loadImage(red, function() {
t.equal(returnval.name,"ImageSequencer Wrapper", "Wrapper is returned"); var returnval = this;
t.equal(returnval.images[0],"image2","Image scope is defined"); t.equal(returnval.name,"ImageSequencer Wrapper", "Wrapper is returned");
t.end(); t.equal(returnval.images[0],"image2","Image scope is defined");
t.end();
});
}); });
test('addSteps is two-way chainable.', function (t){ test('addSteps is two-way chainable.', function (t){
var returnval = sequencer.loadImage(red).addSteps('invert'); sequencer.loadImage(red, function(){
t.equal(returnval.name,"ImageSequencer Wrapper", "Wrapper is returned"); var returnval = this;
t.equal(returnval.images[0],"image3","Image scope is defined"); this.addSteps('invert');
t.equal(sequencer.images.image3.steps.length,2,"Loaded image is affected"); t.equal(returnval.name,"ImageSequencer Wrapper", "Wrapper is returned");
t.equal(sequencer.images.image3.steps[1].options.name,"invert","Correct Step Added"); t.equal(returnval.images[0],"image3","Image scope is defined");
t.equal(sequencer.images.image2.steps.length,1,"Other images are not affected"); t.equal(sequencer.images.image3.steps.length,2,"Loaded image is affected");
t.equal(sequencer.images.image1.steps.length,1,"Other images are not affected"); t.equal(sequencer.images.image3.steps[1].options.name,"invert","Correct Step Added");
t.end(); t.equal(sequencer.images.image2.steps.length,1,"Other images are not affected");
t.equal(sequencer.images.image1.steps.length,1,"Other images are not affected");
t.end();
});
}); });
test('addSteps is two-way chainable without loadImages.', function (t){ test('addSteps is two-way chainable without loadImages.', function (t){
@@ -44,11 +49,14 @@ test('addSteps is two-way chainable without loadImages.', function (t){
}); });
test('removeSteps is two-way chainable.', function (t){ test('removeSteps is two-way chainable.', function (t){
var returnval = sequencer.loadImage(red).addSteps('invert').removeSteps(1); sequencer.loadImage(red,function(){
t.equal(returnval.name,"ImageSequencer Wrapper", "Wrapper is returned"); var returnval = this;
t.equal(returnval.images[0],"image4","Image scope is defined"); this.addSteps('invert').removeSteps(1);
t.equal(sequencer.images.image4.steps.length,1); t.equal(returnval.name,"ImageSequencer Wrapper", "Wrapper is returned");
t.end(); t.equal(returnval.images[0],"image4","Image scope is defined");
t.equal(sequencer.images.image4.steps.length,1);
t.end();
});
}); });
test('removeSteps is two-way chainable without loadImages.', function (t){ test('removeSteps is two-way chainable without loadImages.', function (t){
@@ -59,12 +67,15 @@ test('removeSteps is two-way chainable without loadImages.', function (t){
}); });
test('insertSteps is two-way chainable.', function (t){ test('insertSteps is two-way chainable.', function (t){
var returnval = sequencer.loadImage(red).insertSteps(1,'invert'); sequencer.loadImage(red,function() {
t.equal(returnval.name,"ImageSequencer Wrapper","Wrapper is returned"); var returnval = this;
t.equal(returnval.images[0],"image5","Image scope is defined"); this.insertSteps(1,'invert');
t.equal(sequencer.images.image5.steps.length,2); t.equal(returnval.name,"ImageSequencer Wrapper","Wrapper is returned");
t.equal(sequencer.images.image5.steps[1].options.name,"invert","Correct Step Inserrted"); t.equal(returnval.images[0],"image5","Image scope is defined");
t.end(); t.equal(sequencer.images.image5.steps.length,2);
t.equal(sequencer.images.image5.steps[1].options.name,"invert","Correct Step Inserrted");
t.end();
});
}); });
test('insertSteps is two-way chainable without loadImages.', function (t){ test('insertSteps is two-way chainable without loadImages.', function (t){

View File

@@ -7,12 +7,20 @@ var test = require('tape');
require('../src/ImageSequencer.js'); require('../src/ImageSequencer.js');
var sequencer = ImageSequencer({ ui: "none" }); //require image files as DataURLs so they can be tested alike on browser and Node.
var image = "data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAYEBQYFBAYGBQYHBwYIChAKCgkJChQODwwQFxQYGBcUFhYaHSUfGhsjHBYWICwgIyYnKSopGR8tMC0oMCUoKSj/2wBDAQcHBwoIChMKChMoGhYaKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCj/wAARCAAQABADASIAAhEBAxEB/8QAFQABAQAAAAAAAAAAAAAAAAAAAAf/xAAUEAEAAAAAAAAAAAAAAAAAAAAA/8QAFQEBAQAAAAAAAAAAAAAAAAAABgj/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwCdABykX//Z"; var sequencer = ImageSequencer({ ui: false });
sequencer.loadImages(image);
var test_png = require('../examples/test.png.js');
var test_gif = require('../examples/test.gif.js');
sequencer.loadImages(test_png);
sequencer.addSteps(['do-nothing-pix','invert','invert']); sequencer.addSteps(['do-nothing-pix','invert','invert']);
sequencer.run();
test("Preload", function(t) {
sequencer.run(function(){
t.end();
});
});
test("Inverted image isn't identical", function (t) { test("Inverted image isn't identical", function (t) {
t.notEqual(sequencer.images.image1.steps[1].output.src, sequencer.images.image1.steps[2].output.src); t.notEqual(sequencer.images.image1.steps[1].output.src, sequencer.images.image1.steps[2].output.src);
@@ -23,3 +31,21 @@ test("Twice inverted image is identical to original image", function (t) {
t.equal(sequencer.images.image1.steps[1].output.src, sequencer.images.image1.steps[3].output.src); t.equal(sequencer.images.image1.steps[1].output.src, sequencer.images.image1.steps[3].output.src);
t.end(); t.end();
}); });
test("PixelManipulation works for PNG images", function (t) {
sequencer.loadImages(test_png,function(){
this.addSteps('invert').run(function(out){
t.equal(1,1)
t.end();
});
});
});
test("PixelManipulation works for GIF images", function (t) {
sequencer.loadImages(test_gif,function(){
this.addSteps('invert').run(function(out){
t.equal(1,1)
t.end();
});
});
});

View File

@@ -8,7 +8,24 @@ var test = require('tape');
require('../src/ImageSequencer.js'); require('../src/ImageSequencer.js');
var sequencer = ImageSequencer({ ui: "none" }); // This function is used to test whether or not any additional global variables are being created
function copy(g,a) {
var b = {};
var i = 0;
for (var v in a)
if(g) {
if(v != "sequencer") b[v] = a[v];
}
else {
if(v != "sequencer" && v!="global1" && v!="global2" && !global1.hasOwnProperty(v)) i++;
}
if(g) return b;
else return i;
}
var parent = (typeof(global)==="undefined")?window:global;
var global1 = copy(true,parent);
var sequencer = ImageSequencer({ ui: false });
var red = "data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAYEBQYFBAYGBQYHBwYIChAKCgkJChQODwwQFxQYGBcUFhYaHSUfGhsjHBYWICwgIyYnKSopGR8tMC0oMCUoKSj/2wBDAQcHBwoIChMKChMoGhYaKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCj/wAARCAAQABADASIAAhEBAxEB/8QAFQABAQAAAAAAAAAAAAAAAAAAAAf/xAAUEAEAAAAAAAAAAAAAAAAAAAAA/8QAFQEBAQAAAAAAAAAAAAAAAAAABgj/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwCdABykX//Z"; var red = "data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAYEBQYFBAYGBQYHBwYIChAKCgkJChQODwwQFxQYGBcUFhYaHSUfGhsjHBYWICwgIyYnKSopGR8tMC0oMCUoKSj/2wBDAQcHBwoIChMKChMoGhYaKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCj/wAARCAAQABADASIAAhEBAxEB/8QAFQABAQAAAAAAAAAAAAAAAAAAAAf/xAAUEAEAAAAAAAAAAAAAAAAAAAAA/8QAFQEBAQAAAAAAAAAAAAAAAAAABgj/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwCdABykX//Z";
test('Image Sequencer has tests', function (t) { test('Image Sequencer has tests', function (t) {
@@ -23,18 +40,22 @@ test('loadImages loads a DataURL image and creates a step.', function (t){
t.end(); t.end();
}); });
test('loadImages loads a PATH image and creates a step. (NodeJS)', function (t){ if(!sequencer.options.inBrowser)
if(sequencer.options.inBrowser){ test('loadImage loads an image from URL and creates a step. (NodeJS)', function (t){
t.equal("not applicable","not applicable","Not applicable for Browser"); sequencer.loadImage('URL','https://ccpandhare.github.io/image-sequencer/examples/red.jpg', function(){
t.end(); t.equal(sequencer.images.URL.steps.length, 1, "Initial Step Created");
} t.equal(typeof(sequencer.images.URL.steps[0].output.src), "string", "Initial output exists");
else { t.end();
sequencer.loadImages(red); });
});
if(!sequencer.options.inBrowser)
test('loadImages loads an image from PATH and creates a step. (NodeJS)', function (t){
sequencer.loadImages('examples/red.jpg');
t.equal(sequencer.images.image1.steps.length, 1, "Initial Step Created"); t.equal(sequencer.images.image1.steps.length, 1, "Initial Step Created");
t.equal(typeof(sequencer.images.image1.steps[0].output.src), "string", "Initial output exists"); t.equal(typeof(sequencer.images.image1.steps[0].output.src), "string", "Initial output exists");
t.end(); t.end();
} });
});
test('loadImage works too.', function (t){ test('loadImage works too.', function (t){
sequencer.loadImage('test2',red); sequencer.loadImage('test2',red);
@@ -138,3 +159,9 @@ test('replaceImage returns false in NodeJS', function (t) {
t.equal(returnvalue,false,"It does."); t.equal(returnvalue,false,"It does.");
t.end(); t.end();
}); });
var global2 = copy(false,parent);
test('No Global Variables Created', function (t) {
t.equal(0,global2,"None Created.");
t.end();
});