Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add a boolean operator that performs bitwise boolean operations between images #501

Merged
merged 12 commits into from
Jul 11, 2016
15 changes: 15 additions & 0 deletions docs/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -519,6 +519,21 @@ sharp('input.png')
```

In the above example if `input.png` is a 3 channel RGB image, `output.png` will be a 1 channel grayscale image where each pixel `P = R & G & B`. For example, if `I(1,1) = [247, 170, 14] = [0b11110111, 0b10101010, 0b00001111]` then `O(1,1) = 0b11110111 & 0b10101010 & 0b00001111 = 0b00000010 = 2`.

#### boolean(image, operation)

Perform a bitwise boolean operation with `image`.

`image` is one of the following.

* Buffer contianing PNG, WebP, GIF or SVG image data, or
* String containing the path to an image file

This operation creates an output image where each pixel is the result of the selected bitwise boolean `operation`, between the corresponding pixels of the input images. The boolean operation can be one of the following:

* `and` performs a bitwise and operation, like the c-operator `&`
* `or` performs a bitwise or operation, like the c-operator `|`
* `eor` performs a bitwise exclusive or operation, like the c-operator `^`

### Output

Expand Down
27 changes: 27 additions & 0 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,9 @@ var Sharp = function(input, options) {
greyscale: false,
normalize: 0,
bandBoolOp: null,
booleanOp: null,
booleanBufferIn: null,
booleanFileIn: '',
// overlay
overlayFileIn: '',
overlayBufferIn: null,
Expand Down Expand Up @@ -365,6 +368,30 @@ Sharp.prototype.negate = function(negate) {
return this;
};

/*
Bitwise boolean operations between images
*/
Sharp.prototype.boolean = function(operand, operator) {
if (isString(operand)) {
this.options.booleanFileIn = operand;
} else if (isBuffer(operand)) {
this.options.booleanBufferIn = operand;
} else {
throw new Error('Unsupported boolean operand ' + typeof operand);
}
if (!isString(operator)) {
throw new Error('Invalid boolean operation ' + operator);
}
operator = operator.toLowerCase();
var ops = ['and', 'or', 'eor'];
if(ops.indexOf(operator) == -1) {
throw new Error('Invalid boolean operation ' + operator);
}
this.options.booleanOp = operator;

return this;
};

/*
Overlay with another image, using an optional gravity
*/
Expand Down
9 changes: 9 additions & 0 deletions src/common.cc
Original file line number Diff line number Diff line change
Expand Up @@ -330,4 +330,13 @@ namespace sharp {
}
}

/*
Get VIPS Boolean operatoin type from string
*/
VipsOperationBoolean GetBooleanOperation(std::string opStr) {
return static_cast<VipsOperationBoolean>(
vips_enum_from_nick(nullptr, VIPS_TYPE_OPERATION_BOOLEAN, opStr.data())
);
}

} // namespace sharp
5 changes: 5 additions & 0 deletions src/common.h
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,11 @@ namespace sharp {
*/
int MaximumImageAlpha(VipsInterpretation interpretation);

/*
Get VIPS Boolean operatoin type from string
*/
VipsOperationBoolean GetBooleanOperation(std::string opStr);


} // namespace sharp

Expand Down
7 changes: 7 additions & 0 deletions src/operations.cc
Original file line number Diff line number Diff line change
Expand Up @@ -399,6 +399,13 @@ namespace sharp {
return image.bandbool(boolean);
}

/*
Perform bitwise boolean operation between images
*/
VImage Boolean(VImage image, VImage imageR, VipsOperationBoolean const boolean) {
return image.boolean(imageR, boolean);
}

VImage Trim(VImage image, int const tolerance) {
using sharp::MaximumImageAlpha;
// An equivalent of ImageMagick's -trim in C++ ... automatically remove
Expand Down
5 changes: 5 additions & 0 deletions src/operations.h
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,11 @@ namespace sharp {
*/
VImage Bandbool(VImage image, VipsOperationBoolean const boolean);

/*
Perform bitwise boolean operation between images
*/
VImage Boolean(VImage image, VImage imageR, VipsOperationBoolean const boolean);

/*
Trim an image
*/
Expand Down
99 changes: 77 additions & 22 deletions src/pipeline.cc
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@
#include <node.h>
#include <node_buffer.h>

#include <sstream>
#include <iomanip>

#include "nan.h"

#include "common.h"
Expand Down Expand Up @@ -56,6 +59,7 @@ using sharp::EntropyCrop;
using sharp::TileCache;
using sharp::Threshold;
using sharp::Bandbool;
using sharp::Boolean;
using sharp::Trim;

using sharp::ImageType;
Expand All @@ -78,19 +82,22 @@ using sharp::FreeCallback;
using sharp::CalculateCrop;
using sharp::counterProcess;
using sharp::counterQueue;
using sharp::GetBooleanOperation;

typedef struct BufferContainer_t {
std::string name;
Local<Object> nodeBuf;
} BufferContainer;

class PipelineWorker : public AsyncWorker {
public:
PipelineWorker(Callback *callback, PipelineBaton *baton, Callback *queueListener,
const Local<Object> &bufferIn, const Local<Object> &overlayBufferIn) :
AsyncWorker(callback), baton(baton), queueListener(queueListener) {
if (baton->bufferInLength > 0) {
SaveToPersistent("bufferIn", bufferIn);
}
if (baton->overlayBufferInLength > 0) {
SaveToPersistent("overlayBufferIn", overlayBufferIn);
}
const std::vector<BufferContainer> saveBuffers) :
AsyncWorker(callback), baton(baton), queueListener(queueListener), saveBuffers(saveBuffers) {
for (const BufferContainer buf : saveBuffers) {
SaveToPersistent(buf.name.c_str(), buf.nodeBuf);
}
}
~PipelineWorker() {}

/*
Expand Down Expand Up @@ -784,6 +791,44 @@ class PipelineWorker : public AsyncWorker {
}
}

// Apply bitwise boolean operation between images
if (baton->booleanOp != VIPS_OPERATION_BOOLEAN_LAST &&
(baton->booleanBufferInLength > 0 || !baton->booleanFileIn.empty())) {
VImage booleanImage;
ImageType booleanImageType = ImageType::UNKNOWN;
if (baton->booleanBufferInLength > 0) {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I feel like there's potential for extracting this new_from_buffer/new_from_file logic given it appears in a couple of places now. I'm happy to do this later though; this is more of a note-to-self.

// Buffer input for boolean operation
booleanImageType = DetermineImageType(baton->booleanBufferIn, baton->booleanBufferInLength);
if (booleanImageType != ImageType::UNKNOWN) {
try {
booleanImage = VImage::new_from_buffer(baton->booleanBufferIn, baton->booleanBufferInLength,
nullptr, VImage::option()->set("access", baton->accessMethod));
} catch (...) {
(baton->err).append("Boolean operation buffer has corrupt header");
booleanImageType = ImageType::UNKNOWN;
}
} else {
(baton->err).append("Boolean operation buffer contains unsupported image format");
}
} else if (!baton->booleanFileIn.empty()) {
// File input for boolean operation
booleanImageType = DetermineImageType(baton->booleanFileIn.data());
if (booleanImageType != ImageType::UNKNOWN) {
try {
booleanImage = VImage::new_from_file(baton->booleanFileIn.data(),
VImage::option()->set("access", baton->accessMethod));
} catch (...) {
(baton->err).append("Boolean operation file has corrupt header");
}
}
}
if (booleanImageType == ImageType::UNKNOWN) {
return Error();
}
// Apply the boolean operation
image = Boolean(image, booleanImage, baton->booleanOp);
}

// Apply per-channel Bandbool bitwise operations after all other operations
if (shouldBandbool) {
image = Bandbool(image, baton->bandBoolOp);
Expand Down Expand Up @@ -1007,11 +1052,8 @@ class PipelineWorker : public AsyncWorker {
}

// Dispose of Persistent wrapper around input Buffers so they can be garbage collected
if (baton->bufferInLength > 0) {
GetFromPersistent("bufferIn");
}
if (baton->overlayBufferInLength > 0) {
GetFromPersistent("overlayBufferIn");
for (const BufferContainer buf : saveBuffers) {
GetFromPersistent(buf.name.c_str());
}
delete baton;

Expand All @@ -1028,6 +1070,7 @@ class PipelineWorker : public AsyncWorker {
private:
PipelineBaton *baton;
Callback *queueListener;
std::vector<BufferContainer> saveBuffers;

/*
Calculate the angle of rotation and need-to-flip for the output image.
Expand Down Expand Up @@ -1155,6 +1198,14 @@ NAN_METHOD(pipeline) {
baton->overlayYOffset = attrAs<int32_t>(options, "overlayYOffset");
baton->overlayTile = attrAs<bool>(options, "overlayTile");
baton->overlayCutout = attrAs<bool>(options, "overlayCutout");
// Boolean options
baton->booleanFileIn = attrAsStr(options, "booleanFileIn");
Local<Object> booleanBufferIn;
if (node::Buffer::HasInstance(Get(options, New("booleanBufferIn").ToLocalChecked()).ToLocalChecked())) {
booleanBufferIn = Get(options, New("booleanBufferIn").ToLocalChecked()).ToLocalChecked().As<Object>();
baton->booleanBufferInLength = node::Buffer::Length(booleanBufferIn);
baton->booleanBufferIn = node::Buffer::Data(booleanBufferIn);
}
// Resize options
baton->withoutEnlargement = attrAs<bool>(options, "withoutEnlargement");
baton->crop = attrAs<int32_t>(options, "crop");
Expand Down Expand Up @@ -1232,14 +1283,10 @@ NAN_METHOD(pipeline) {
}
}
// Bandbool operation
std::string opStr = attrAsStr(options, "bandBoolOp");
if(opStr == "and" ) {
baton->bandBoolOp = VIPS_OPERATION_BOOLEAN_AND;
} else if(opStr == "or") {
baton->bandBoolOp = VIPS_OPERATION_BOOLEAN_OR;
} else if(opStr == "eor") {
baton->bandBoolOp = VIPS_OPERATION_BOOLEAN_EOR;
}
baton->bandBoolOp = GetBooleanOperation(attrAsStr(options, "bandBoolOp"));

// Boolean operation
baton->booleanOp = GetBooleanOperation(attrAsStr(options, "booleanOp"));

// Function to notify of queue length changes
Callback *queueListener = new Callback(
Expand All @@ -1248,7 +1295,15 @@ NAN_METHOD(pipeline) {

// Join queue for worker thread
Callback *callback = new Callback(info[1].As<Function>());
AsyncQueueWorker(new PipelineWorker(callback, baton, queueListener, bufferIn, overlayBufferIn));

std::vector<BufferContainer> saveBuffers;
if (baton->bufferInLength)
saveBuffers.push_back({"bufferIn", bufferIn});
if (baton->overlayBufferInLength)
saveBuffers.push_back({"overlayBufferIn", overlayBufferIn});
if (baton->booleanBufferInLength)
saveBuffers.push_back({"booleanBufferIn", booleanBufferIn});
AsyncQueueWorker(new PipelineWorker(callback, baton, queueListener, saveBuffers));

// Increment queued task counter
g_atomic_int_inc(&counterQueue);
Expand Down
6 changes: 6 additions & 0 deletions src/pipeline.h
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,9 @@ struct PipelineBaton {
int overlayYOffset;
bool overlayTile;
bool overlayCutout;
std::string booleanFileIn;
char *booleanBufferIn;
size_t booleanBufferInLength;
int topOffsetPre;
int leftOffsetPre;
int widthPre;
Expand Down Expand Up @@ -94,6 +97,7 @@ struct PipelineBaton {
double convKernelScale;
double convKernelOffset;
VipsOperationBoolean bandBoolOp;
VipsOperationBoolean booleanOp;
int extractChannel;
int tileSize;
int tileOverlap;
Expand All @@ -116,6 +120,7 @@ struct PipelineBaton {
overlayYOffset(-1),
overlayTile(false),
overlayCutout(false),
booleanBufferInLength(0),
topOffsetPre(-1),
topOffsetPost(-1),
channels(0),
Expand Down Expand Up @@ -156,6 +161,7 @@ struct PipelineBaton {
convKernelScale(0.0),
convKernelOffset(0.0),
bandBoolOp(VIPS_OPERATION_BOOLEAN_LAST),
booleanOp(VIPS_OPERATION_BOOLEAN_LAST),
extractChannel(-1),
tileSize(256),
tileOverlap(0),
Expand Down
Binary file added test/fixtures/booleanTest.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added test/fixtures/expected/boolean_and_result.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added test/fixtures/expected/boolean_eor_result.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added test/fixtures/expected/boolean_or_result.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 2 additions & 0 deletions test/fixtures/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,8 @@ module.exports = {
inputPngStripesV: getPath('stripesV.png'),
inputPngStripesH: getPath('stripesH.png'),

inputJpgBooleanTest: getPath('booleanTest.jpg'),

inputV: getPath('vfile.v'),

outputJpg: getPath('output.jpg'),
Expand Down
Loading