mirror of
https://github.com/lovell/sharp.git
synced 2025-07-09 10:30:15 +02:00
Add boolean feature for bitwise image operations (#501)
This commit is contained in:
parent
99f960bf56
commit
d17e8d3450
15
docs/api.md
15
docs/api.md
@ -520,6 +520,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
|
||||
|
||||
#### toFile(path, [callback])
|
||||
|
27
index.js
27
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
|
||||
*/
|
||||
|
@ -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
|
||||
|
@ -123,6 +123,11 @@ namespace sharp {
|
||||
*/
|
||||
int MaximumImageAlpha(VipsInterpretation interpretation);
|
||||
|
||||
/*
|
||||
Get VIPS Boolean operatoin type from string
|
||||
*/
|
||||
VipsOperationBoolean GetBooleanOperation(std::string opStr);
|
||||
|
||||
|
||||
} // namespace sharp
|
||||
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
*/
|
||||
|
@ -9,6 +9,9 @@
|
||||
#include <node.h>
|
||||
#include <node_buffer.h>
|
||||
|
||||
#include <sstream>
|
||||
#include <iomanip>
|
||||
|
||||
#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<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() {}
|
||||
|
||||
/*
|
||||
@ -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<BufferContainer> saveBuffers;
|
||||
|
||||
/*
|
||||
Calculate the angle of rotation and need-to-flip for the output image.
|
||||
@ -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");
|
||||
@ -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<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);
|
||||
|
@ -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),
|
||||
|
BIN
test/fixtures/booleanTest.jpg
vendored
Normal file
BIN
test/fixtures/booleanTest.jpg
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 11 KiB |
BIN
test/fixtures/expected/boolean_and_result.jpg
vendored
Normal file
BIN
test/fixtures/expected/boolean_and_result.jpg
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 14 KiB |
BIN
test/fixtures/expected/boolean_eor_result.jpg
vendored
Normal file
BIN
test/fixtures/expected/boolean_eor_result.jpg
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 23 KiB |
BIN
test/fixtures/expected/boolean_or_result.jpg
vendored
Normal file
BIN
test/fixtures/expected/boolean_or_result.jpg
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 17 KiB |
2
test/fixtures/index.js
vendored
2
test/fixtures/index.js
vendored
@ -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'),
|
||||
|
103
test/unit/boolean.js
Normal file
103
test/unit/boolean.js
Normal file
@ -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');
|
||||
});
|
||||
});
|
||||
});
|
Loading…
x
Reference in New Issue
Block a user