diff --git a/docs/api.md b/docs/api.md index 193e412c..5bcded52 100644 --- a/docs/api.md +++ b/docs/api.md @@ -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 diff --git a/index.js b/index.js index 937d3ba3..52df60ce 100644 --- a/index.js +++ b/index.js @@ -91,6 +91,9 @@ var Sharp = function(input, options) { greyscale: false, normalize: 0, bandBoolOp: null, + booleanOp: null, + booleanBufferIn: null, + booleanFileIn: '', // overlay overlayFileIn: '', overlayBufferIn: null, @@ -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 */ diff --git a/src/common.cc b/src/common.cc index 1045c39e..ae0de8cb 100644 --- a/src/common.cc +++ b/src/common.cc @@ -330,4 +330,13 @@ namespace sharp { } } + /* + Get VIPS Boolean operatoin type from string + */ + VipsOperationBoolean GetBooleanOperation(std::string opStr) { + return static_cast( + vips_enum_from_nick(nullptr, VIPS_TYPE_OPERATION_BOOLEAN, opStr.data()) + ); + } + } // namespace sharp diff --git a/src/common.h b/src/common.h index 511fc804..e2774291 100644 --- a/src/common.h +++ b/src/common.h @@ -123,6 +123,11 @@ namespace sharp { */ int MaximumImageAlpha(VipsInterpretation interpretation); + /* + Get VIPS Boolean operatoin type from string + */ + VipsOperationBoolean GetBooleanOperation(std::string opStr); + } // namespace sharp diff --git a/src/operations.cc b/src/operations.cc index 58adba1b..8c7987c4 100644 --- a/src/operations.cc +++ b/src/operations.cc @@ -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 diff --git a/src/operations.h b/src/operations.h index 41a8d0f9..8f3cd9f3 100644 --- a/src/operations.h +++ b/src/operations.h @@ -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 */ diff --git a/src/pipeline.cc b/src/pipeline.cc index 99ef7a10..64c8f4d7 100644 --- a/src/pipeline.cc +++ b/src/pipeline.cc @@ -9,6 +9,9 @@ #include #include +#include +#include + #include "nan.h" #include "common.h" @@ -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; @@ -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 nodeBuf; +} BufferContainer; class PipelineWorker : public AsyncWorker { public: PipelineWorker(Callback *callback, PipelineBaton *baton, Callback *queueListener, - const Local &bufferIn, const Local &overlayBufferIn) : - AsyncWorker(callback), baton(baton), queueListener(queueListener) { - if (baton->bufferInLength > 0) { - SaveToPersistent("bufferIn", bufferIn); - } - if (baton->overlayBufferInLength > 0) { - SaveToPersistent("overlayBufferIn", overlayBufferIn); - } + const std::vector saveBuffers) : + AsyncWorker(callback), baton(baton), queueListener(queueListener), saveBuffers(saveBuffers) { + for (const BufferContainer buf : saveBuffers) { + SaveToPersistent(buf.name.c_str(), buf.nodeBuf); } + } ~PipelineWorker() {} /* @@ -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) { + // 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); @@ -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; @@ -1028,6 +1070,7 @@ class PipelineWorker : public AsyncWorker { private: PipelineBaton *baton; Callback *queueListener; + std::vector saveBuffers; /* Calculate the angle of rotation and need-to-flip for the output image. @@ -1155,6 +1198,14 @@ NAN_METHOD(pipeline) { baton->overlayYOffset = attrAs(options, "overlayYOffset"); baton->overlayTile = attrAs(options, "overlayTile"); baton->overlayCutout = attrAs(options, "overlayCutout"); + // Boolean options + baton->booleanFileIn = attrAsStr(options, "booleanFileIn"); + Local booleanBufferIn; + if (node::Buffer::HasInstance(Get(options, New("booleanBufferIn").ToLocalChecked()).ToLocalChecked())) { + booleanBufferIn = Get(options, New("booleanBufferIn").ToLocalChecked()).ToLocalChecked().As(); + baton->booleanBufferInLength = node::Buffer::Length(booleanBufferIn); + baton->booleanBufferIn = node::Buffer::Data(booleanBufferIn); + } // Resize options baton->withoutEnlargement = attrAs(options, "withoutEnlargement"); baton->crop = attrAs(options, "crop"); @@ -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( @@ -1248,7 +1295,15 @@ NAN_METHOD(pipeline) { // Join queue for worker thread Callback *callback = new Callback(info[1].As()); - AsyncQueueWorker(new PipelineWorker(callback, baton, queueListener, bufferIn, overlayBufferIn)); + + std::vector 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); diff --git a/src/pipeline.h b/src/pipeline.h index c6a64f5a..68961fd5 100644 --- a/src/pipeline.h +++ b/src/pipeline.h @@ -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; @@ -94,6 +97,7 @@ struct PipelineBaton { double convKernelScale; double convKernelOffset; VipsOperationBoolean bandBoolOp; + VipsOperationBoolean booleanOp; int extractChannel; int tileSize; int tileOverlap; @@ -116,6 +120,7 @@ struct PipelineBaton { overlayYOffset(-1), overlayTile(false), overlayCutout(false), + booleanBufferInLength(0), topOffsetPre(-1), topOffsetPost(-1), channels(0), @@ -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), diff --git a/test/fixtures/booleanTest.jpg b/test/fixtures/booleanTest.jpg new file mode 100644 index 00000000..69835074 Binary files /dev/null and b/test/fixtures/booleanTest.jpg differ diff --git a/test/fixtures/expected/boolean_and_result.jpg b/test/fixtures/expected/boolean_and_result.jpg new file mode 100644 index 00000000..cdc13019 Binary files /dev/null and b/test/fixtures/expected/boolean_and_result.jpg differ diff --git a/test/fixtures/expected/boolean_eor_result.jpg b/test/fixtures/expected/boolean_eor_result.jpg new file mode 100644 index 00000000..bae4be8a Binary files /dev/null and b/test/fixtures/expected/boolean_eor_result.jpg differ diff --git a/test/fixtures/expected/boolean_or_result.jpg b/test/fixtures/expected/boolean_or_result.jpg new file mode 100644 index 00000000..a95a2962 Binary files /dev/null and b/test/fixtures/expected/boolean_or_result.jpg differ diff --git a/test/fixtures/index.js b/test/fixtures/index.js index 65df6673..66facc28 100644 --- a/test/fixtures/index.js +++ b/test/fixtures/index.js @@ -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'), diff --git a/test/unit/boolean.js b/test/unit/boolean.js new file mode 100644 index 00000000..7f601de2 --- /dev/null +++ b/test/unit/boolean.js @@ -0,0 +1,103 @@ +'use strict'; + +var fs = require('fs'); +var assert = require('assert'); +var fixtures = require('../fixtures'); +var sharp = require('../../index'); + +describe('Boolean operation between two images', function() { + + it('\'and\' Operation, file', function(done) { + sharp(fixtures.inputJpg) //fixtures.inputJpg + .resize(320,240) + .boolean(fixtures.inputJpgBooleanTest, 'and') + .toBuffer(function(err, data, info) { + if (err) throw err; + assert.strictEqual(320, info.width); + assert.strictEqual(240, info.height); + fixtures.assertSimilar(fixtures.expected('boolean_and_result.jpg'), data, done); + }); + }); + + it('\'and\' Operation, buffer', function(done) { + sharp(fixtures.inputJpg) //fixtures.inputJpg + .resize(320,240) + .boolean(fs.readFileSync(fixtures.inputJpgBooleanTest), sharp.bool.and) + .toBuffer(function(err, data, info) { + if (err) throw err; + assert.strictEqual(320, info.width); + assert.strictEqual(240, info.height); + fixtures.assertSimilar(fixtures.expected('boolean_and_result.jpg'), data, done); + }); + }); + + it('\'or\' Operation, file', function(done) { + sharp(fixtures.inputJpg) + .resize(320,240) + .boolean(fixtures.inputJpgBooleanTest, sharp.bool.or) + .toBuffer(function(err, data, info) { + if (err) throw err; + assert.strictEqual(320, info.width); + assert.strictEqual(240, info.height); + fixtures.assertSimilar(fixtures.expected('boolean_or_result.jpg'), data, done); + }); + }); + + it('\'or\' Operation, buffer', function(done) { + sharp(fixtures.inputJpg) + .resize(320,240) + .boolean(fs.readFileSync(fixtures.inputJpgBooleanTest), 'or') + .toBuffer(function(err, data, info) { + if (err) throw err; + assert.strictEqual(320, info.width); + assert.strictEqual(240, info.height); + fixtures.assertSimilar(fixtures.expected('boolean_or_result.jpg'), data, done); + }); + }); + + it('\'eor\' Operation, file', function(done) { + sharp(fixtures.inputJpg) + .resize(320,240) + .boolean(fixtures.inputJpgBooleanTest, 'eor') + .toBuffer(function(err, data, info) { + if (err) throw err; + assert.strictEqual(320, info.width); + assert.strictEqual(240, info.height); + fixtures.assertSimilar(fixtures.expected('boolean_eor_result.jpg'), data, done); + }); + }); + + it('\'eor\' Operation, buffer', function(done) { + sharp(fixtures.inputJpg) + .resize(320,240) + .boolean(fs.readFileSync(fixtures.inputJpgBooleanTest), sharp.bool.eor) + .toBuffer(function(err, data, info) { + if (err) throw err; + assert.strictEqual(320, info.width); + assert.strictEqual(240, info.height); + fixtures.assertSimilar(fixtures.expected('boolean_eor_result.jpg'), data, done); + }); + }); + + it('Invalid operation', function() { + assert.throws(function() { + sharp(fixtures.inputJpg) + .boolean(fs.readFileSync(fixtures.inputJpgBooleanTest), 'fail'); + }); + }); + + it('Invalid operation, non-string', function() { + assert.throws(function() { + sharp(fixtures.inputJpg) + .boolean(fs.readFileSync(fixtures.inputJpgBooleanTest), null); + }); + }); + + if('Invalid buffer input', function() { + assert.throws(function() { + sharp(fixtures.inputJpg) + .resize(320,240) + .boolean([],'eor'); + }); + }); +});