diff --git a/docs/api.md b/docs/api.md index 139a4e38..564df25a 100644 --- a/docs/api.md +++ b/docs/api.md @@ -483,6 +483,24 @@ sharp('input.png') }); ``` +#### bandbool(operation) + +Perform a bitwise boolean operation on image color channels (bands in vips terminology). The result is a single channel grayscale image. Bandbool is performed at the end of the image processing pipeline, after gamma correction, colorspace conversion, normalization, and other operations. This makes it possible to create an image that contains the unaltered result of the boolean operation. Note that the alpha channel of the image is included in `bandbool` operations. All channels are cast to an integer type before the operation. `bandbool` takes no effect on single channel images. + +`operation` is a string containing the name of the bitwise operator to be appled to image color channels, which can be one of: + + * `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 `^` + +```javascript +sharp('input.png') + .bandbool(sharp.bool.and) + .toFile('output.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`. + ### Output #### toFile(path, [callback]) diff --git a/index.js b/index.js index 93a0a37c..2bde9ce4 100644 --- a/index.js +++ b/index.js @@ -89,6 +89,7 @@ var Sharp = function(input, options) { gamma: 0, greyscale: false, normalize: 0, + bandBoolOp: null, // overlay overlayFileIn: '', overlayBufferIn: null, @@ -588,6 +589,22 @@ Sharp.prototype.normalize = function(normalize) { }; Sharp.prototype.normalise = Sharp.prototype.normalize; +/* + Perform boolean/bitwise operation on image color channels - results in one channel image +*/ +Sharp.prototype.bandbool = function(boolOp) { + if(typeof boolOp !== 'string') { + throw new Error('Invalid bandbool operation'); + } + boolOp = boolOp.toLowerCase(); + var ops = ['and', 'or', 'eor']; + if(ops.indexOf(boolOp) == -1) { + throw new Error('Invalid bandbool operation'); + } + this.options.bandBoolOp = boolOp; + return this; +}; + /* Convert to greyscale */ @@ -785,6 +802,12 @@ module.exports.interpolator = { vsqbs: 'vsqbs', vertexSplitQuadraticBasisSpline: 'vsqbs' }; +// Boolean operations for bandbool +module.exports.bool = { + and: 'and', + or: 'or', + eor: 'eor' +}; /* Resize image to width x height pixels diff --git a/src/operations.cc b/src/operations.cc index 106c5a04..e85f5730 100644 --- a/src/operations.cc +++ b/src/operations.cc @@ -392,4 +392,11 @@ namespace sharp { return image.colourspace(VIPS_INTERPRETATION_B_W) >= threshold; } + /* + Perform boolean/bitwise operation on image color channels - results in one channel image + */ + VImage Bandbool(VImage image, VipsOperationBoolean const boolean) { + return image.bandbool(boolean); + } + } // namespace sharp diff --git a/src/operations.h b/src/operations.h index dee7dbe4..8d1f9122 100644 --- a/src/operations.h +++ b/src/operations.h @@ -82,6 +82,11 @@ namespace sharp { */ VImage Threshold(VImage image, double const threshold, bool const thresholdColor); + /* + Perform boolean/bitwise operation on image color channels - results in one channel image + */ + VImage Bandbool(VImage image, VipsOperationBoolean const boolean); + } // namespace sharp #endif // SRC_OPERATIONS_H_ diff --git a/src/pipeline.cc b/src/pipeline.cc index d0b1b77c..f643190c 100644 --- a/src/pipeline.cc +++ b/src/pipeline.cc @@ -55,6 +55,7 @@ using sharp::Sharpen; using sharp::EntropyCrop; using sharp::TileCache; using sharp::Threshold; +using sharp::Bandbool; using sharp::ImageType; using sharp::ImageTypeId; @@ -470,6 +471,8 @@ class PipelineWorker : public AsyncWorker { bool shouldSharpen = baton->sharpenSigma != 0.0; bool shouldThreshold = baton->threshold != 0; bool shouldCutout = baton->overlayCutout; + bool shouldBandbool = baton->bandBoolOp < VIPS_OPERATION_BOOLEAN_LAST && + baton->bandBoolOp >= VIPS_OPERATION_BOOLEAN_AND; bool shouldPremultiplyAlpha = HasAlpha(image) && (shouldAffineTransform || shouldBlur || shouldConv || shouldSharpen || (hasOverlay && !shouldCutout)); @@ -774,6 +777,11 @@ class PipelineWorker : public AsyncWorker { } } + // Apply per-channel Bandbool bitwise operations after all other operations + if (shouldBandbool) { + image = Bandbool(image, baton->bandBoolOp); + } + // Override EXIF Orientation tag if (baton->withMetadata && baton->withMetadataOrientation != -1) { SetExifOrientation(image, baton->withMetadataOrientation); @@ -1194,6 +1202,15 @@ NAN_METHOD(pipeline) { baton->convKernel[i] = To(Get(kdata, i).ToLocalChecked()).FromJust(); } } + // 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; + } // Function to notify of queue length changes Callback *queueListener = new Callback( diff --git a/src/pipeline.h b/src/pipeline.h index 96a1705c..a1807837 100644 --- a/src/pipeline.h +++ b/src/pipeline.h @@ -92,6 +92,7 @@ struct PipelineBaton { int convKernelHeight; double convKernelScale; double convKernelOffset; + VipsOperationBoolean bandBoolOp; int tileSize; int tileOverlap; VipsForeignDzContainer tileContainer; @@ -151,6 +152,7 @@ struct PipelineBaton { convKernelHeight(0), convKernelScale(0.0), convKernelOffset(0.0), + bandBoolOp(VIPS_OPERATION_BOOLEAN_LAST), tileSize(256), tileOverlap(0), tileContainer(VIPS_FOREIGN_DZ_CONTAINER_FS), diff --git a/test/fixtures/bandbool.png b/test/fixtures/bandbool.png new file mode 100644 index 00000000..39023cdf Binary files /dev/null and b/test/fixtures/bandbool.png differ diff --git a/test/fixtures/expected/bandbool_and_result.png b/test/fixtures/expected/bandbool_and_result.png new file mode 100644 index 00000000..722716be Binary files /dev/null and b/test/fixtures/expected/bandbool_and_result.png differ diff --git a/test/fixtures/expected/bandbool_eor_result.png b/test/fixtures/expected/bandbool_eor_result.png new file mode 100644 index 00000000..35b2920a Binary files /dev/null and b/test/fixtures/expected/bandbool_eor_result.png differ diff --git a/test/fixtures/expected/bandbool_or_result.png b/test/fixtures/expected/bandbool_or_result.png new file mode 100644 index 00000000..3973cc1a Binary files /dev/null and b/test/fixtures/expected/bandbool_or_result.png differ diff --git a/test/fixtures/index.js b/test/fixtures/index.js index 231c8b3b..3555a93c 100644 --- a/test/fixtures/index.js +++ b/test/fixtures/index.js @@ -79,6 +79,7 @@ module.exports = { inputPngOverlayLayer2LowAlpha: getPath('alpha-layer-2-ink-low-alpha.png'), inputPngAlphaPremultiplicationSmall: getPath('alpha-premultiply-1024x768-paper.png'), inputPngAlphaPremultiplicationLarge: getPath('alpha-premultiply-2048x1536-paper.png'), + inputPngBooleanNoAlpha: getPath('bandbool.png'), inputWebP: getPath('4.webp'), // http://www.gstatic.com/webp/gallery/4.webp inputWebPWithTransparency: getPath('5_webp_a.webp'), // http://www.gstatic.com/webp/gallery3/5_webp_a.webp diff --git a/test/unit/bandbool.js b/test/unit/bandbool.js new file mode 100644 index 00000000..725e4542 --- /dev/null +++ b/test/unit/bandbool.js @@ -0,0 +1,51 @@ +'use strict'; + +var assert = require('assert'); +var fixtures = require('../fixtures'); +var sharp = require('../../index'); + +describe('Bandbool per-channel boolean operations', function() { + + it('\'and\' Operation', function(done) { + sharp(fixtures.inputPngBooleanNoAlpha) + .bandbool('and') + .toBuffer(function(err, data, info) { + if (err) throw err; + assert.strictEqual(200, info.width); + assert.strictEqual(200, info.height); + assert.strictEqual(1, info.channels); + fixtures.assertSimilar(fixtures.expected('bandbool_and_result.png'), data, done); + }); + }); + + it('\'or\' Operation', function(done) { + sharp(fixtures.inputPngBooleanNoAlpha) + .bandbool(sharp.bool.or) + .toBuffer(function(err, data, info) { + if (err) throw err; + assert.strictEqual(200, info.width); + assert.strictEqual(200, info.height); + assert.strictEqual(1, info.channels); + fixtures.assertSimilar(fixtures.expected('bandbool_or_result.png'), data, done); + }); + }); + + it('\'eor\' Operation', function(done) { + sharp(fixtures.inputPngBooleanNoAlpha) + .bandbool('eor') + .toBuffer(function(err, data, info) { + if (err) throw err; + assert.strictEqual(200, info.width); + assert.strictEqual(200, info.height); + assert.strictEqual(1, info.channels); + fixtures.assertSimilar(fixtures.expected('bandbool_eor_result.png'), data, done); + }); + }); + + it('Invalid operation', function() { + assert.throws(function() { + sharp(fixtures.inputPngBooleanNoAlpha) + .bandbool('fail'); + }); + }); +});