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

boolean() and overlayWith() accept raw buffers as input #516

Closed
wants to merge 5 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 6 additions & 3 deletions docs/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -457,7 +457,7 @@ Overlay (composite) a image containing an alpha channel over the processed (resi

`image` is one of the following, and must be the same size or smaller than the processed image:

* Buffer containing PNG, WebP, GIF or SVG image data, or
* Buffer containing PNG, WebP, GIF, SVG, or RAW image data, or
* String containing the path to an image file, with most major transparency formats supported.

`options`, if present, is an Object with the following optional attributes:
Expand All @@ -467,6 +467,7 @@ Overlay (composite) a image containing an alpha channel over the processed (resi
* `left` is an integral Number representing the pixel offset from the left edge.
* `tile` is a Boolean, defaulting to `false`. When set to `true` repeats the overlay image across the entire image with the given `gravity`.
* `cutout` is a Boolean, defaulting to `false`. When set to `true` applies only the alpha channel of the overlay image to the image to be overlaid, giving the appearance of one image being cut out of another.
* `raw` is a required attribute when using a RAW formatted input buffer. It is an object containing `width`, `height` and `channels` attributes describing the raw buffer. This syntax mirrors the input RAW buffer syntax in the `sharp()` constructor. See `raw()` for pixel ordering.

If both `top` and `left` are provided, they take precedence over `gravity`.

Expand Down Expand Up @@ -523,11 +524,11 @@ 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)
#### boolean(image, operation, [options])

Perform a bitwise boolean operation with `image`, where `image` is one of the following:

* Buffer containing PNG, WebP, GIF or SVG image data, or
* Buffer containing PNG, WebP, GIF, SVG or RAW 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.
Expand All @@ -537,6 +538,8 @@ The boolean operation can be one of the following:
* `or` performs a bitwise or operation, like the c-operator `|`.
* `eor` performs a bitwise exclusive or operation, like the c-operator `^`.

To use RAW input, the `options` argument is required to contain an attribute `raw`, an object containing `width`, `height` and `channels` attributes describing the raw buffer. This syntax mirrors the input RAW buffer syntax in the `sharp()` constructor. See `raw()` for pixel ordering.

### Output

#### toFile(path, [callback])
Expand Down
43 changes: 42 additions & 1 deletion index.js
Original file line number Diff line number Diff line change
Expand Up @@ -90,8 +90,12 @@ var Sharp = function(input, options) {
gamma: 0,
greyscale: false,
normalize: 0,
// boolean
booleanBufferIn: null,
booleanFileIn: '',
booleanRawWidth: 0,
booleanRawHeight: 0,
booleanRawChannels: 0,
// overlay
overlayFileIn: '',
overlayBufferIn: null,
Expand All @@ -100,6 +104,9 @@ var Sharp = function(input, options) {
overlayYOffset : -1,
overlayTile: false,
overlayCutout: false,
overlayRawWidth: 0,
overlayRawHeight: 0,
overlayRawChannels: 0,
// output options
formatOut: 'input',
fileOut: '',
Expand Down Expand Up @@ -369,7 +376,7 @@ Sharp.prototype.negate = function(negate) {
/*
Bitwise boolean operations between images
*/
Sharp.prototype.boolean = function(operand, operator) {
Sharp.prototype.boolean = function(operand, operator, options) {
if (isString(operand)) {
this.options.booleanFileIn = operand;
} else if (isBuffer(operand)) {
Expand All @@ -382,13 +389,32 @@ Sharp.prototype.boolean = function(operand, operator) {
} else {
throw new Error('Invalid boolean operation ' + operator);
}
if (isObject(options)) {
if (isDefined(options.raw)) {
if (
isObject(options.raw) &&
isInteger(options.raw.width) && inRange(options.raw.width, 1, maximum.width) &&
isInteger(options.raw.height) && inRange(options.raw.height, 1, maximum.height) &&
isInteger(options.raw.channels) && inRange(options.raw.channels, 1, 4)
) {
this.options.booleanRawWidth = options.raw.width;
this.options.booleanRawHeight = options.raw.height;
this.options.booleanRawChannels = options.raw.channels;
} else {
throw new Error('Boolean: expected width, height and channels for raw pixel input');
}
}
}

return this;
};

/*
Overlay with another image, using an optional gravity
*/
Sharp.prototype.overlayWith = function(overlay, options) {
/* jshint maxcomplexity:16 */
Copy link
Owner

Choose a reason for hiding this comment

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

We'll need much higher test coverage at this complexity level. However a better approach here is probably to abstract the common logic for the various file/Buffer/raw input to a separate function and keep the boolean and overlayWith functions themselves simpler.


if (isString(overlay)) {
this.options.overlayFileIn = overlay;
} else if (isBuffer(overlay)) {
Expand Down Expand Up @@ -431,6 +457,21 @@ Sharp.prototype.overlayWith = function(overlay, options) {
throw new Error('Unsupported overlay gravity ' + options.gravity);
}
}
if (isDefined(options.raw)) {
if (
isObject(options.raw) &&
isInteger(options.raw.width) && inRange(options.raw.width, 1, maximum.width) &&
isInteger(options.raw.height) && inRange(options.raw.height, 1, maximum.height) &&
isInteger(options.raw.channels) && inRange(options.raw.channels, 1, 4)
) {
this.options.overlayRawWidth = options.raw.width;
this.options.overlayRawHeight = options.raw.height;
this.options.overlayRawChannels = options.raw.channels;
} else {
throw new Error('Overlay: expected width, height and channels for raw pixel input');
}
}

}
return this;
};
Expand Down
74 changes: 58 additions & 16 deletions src/pipeline.cc
Original file line number Diff line number Diff line change
Expand Up @@ -683,18 +683,35 @@ class PipelineWorker : public AsyncWorker {
VImage overlayImage;
ImageType overlayImageType = ImageType::UNKNOWN;
if (baton->overlayBufferInLength > 0) {
// Overlay with image from buffer
overlayImageType = DetermineImageType(baton->overlayBufferIn, baton->overlayBufferInLength);
if (overlayImageType != ImageType::UNKNOWN) {
if (baton->overlayRawWidth > 0 && baton->overlayRawHeight > 0 && baton->overlayRawChannels > 0) {
// Raw, uncompressed pixel data
try {
overlayImage = VImage::new_from_buffer(baton->overlayBufferIn, baton->overlayBufferInLength,
nullptr, VImage::option()->set("access", baton->accessMethod));
} catch (...) {
(baton->err).append("Overlay buffer has corrupt header");
overlayImageType = ImageType::UNKNOWN;
overlayImage = VImage::new_from_memory(baton->overlayBufferIn, baton->overlayBufferInLength,
baton->overlayRawWidth, baton->overlayRawHeight, baton->overlayRawChannels, VIPS_FORMAT_UCHAR);
if (baton->overlayRawChannels < 3) {
overlayImage.get_image()->Type = VIPS_INTERPRETATION_B_W;
} else {
overlayImage.get_image()->Type = VIPS_INTERPRETATION_sRGB;
}
overlayImageType = ImageType::RAW;
} catch(VError const &err) {
(baton->err).append(err.what());
inputImageType = ImageType::UNKNOWN;
}
} else {
(baton->err).append("Overlay buffer contains unsupported image format");
// Overlay with image from buffer
overlayImageType = DetermineImageType(baton->overlayBufferIn, baton->overlayBufferInLength);
if (overlayImageType != ImageType::UNKNOWN) {
try {
overlayImage = VImage::new_from_buffer(baton->overlayBufferIn, baton->overlayBufferInLength,
nullptr, VImage::option()->set("access", baton->accessMethod));
} catch (...) {
(baton->err).append("Overlay buffer has corrupt header");
overlayImageType = ImageType::UNKNOWN;
}
} else {
(baton->err).append("Overlay buffer contains unsupported image format");
}
}
} else {
// Overlay with image from file
Expand Down Expand Up @@ -794,17 +811,35 @@ class PipelineWorker : public AsyncWorker {
ImageType booleanImageType = ImageType::UNKNOWN;
if (baton->booleanBufferInLength > 0) {
// Buffer input for boolean operation
booleanImageType = DetermineImageType(baton->booleanBufferIn, baton->booleanBufferInLength);
if (booleanImageType != ImageType::UNKNOWN) {
if (baton->booleanRawWidth > 0 && baton->booleanRawHeight > 0 && baton->booleanRawChannels > 0) {
// Raw, uncompressed pixel data
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");
booleanImage = VImage::new_from_memory(baton->booleanBufferIn, baton->booleanBufferInLength,
baton->booleanRawWidth, baton->booleanRawHeight, baton->booleanRawChannels, VIPS_FORMAT_UCHAR);
if (baton->booleanRawChannels < 3) {
booleanImage.get_image()->Type = VIPS_INTERPRETATION_B_W;
} else {
booleanImage.get_image()->Type = VIPS_INTERPRETATION_sRGB;
}
booleanImageType = ImageType::RAW;
} catch(VError const &err) {
(baton->err).append(err.what());
booleanImageType = ImageType::UNKNOWN;
}
} else {
(baton->err).append("Boolean operation buffer contains unsupported image format");
// Compressed data
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
Expand Down Expand Up @@ -862,6 +897,7 @@ class PipelineWorker : public AsyncWorker {
baton->channels = image.bands();
baton->width = image.width();
baton->height = image.height();

// Output
if (baton->fileOut == "") {
// Buffer output
Expand Down Expand Up @@ -1216,6 +1252,9 @@ NAN_METHOD(pipeline) {
baton->overlayBufferInLength = node::Buffer::Length(overlayBufferIn);
baton->overlayBufferIn = node::Buffer::Data(overlayBufferIn);
buffersToPersist.push_back(overlayBufferIn);
baton->overlayRawWidth = attrAs<int32_t>(options, "overlayRawWidth");
baton->overlayRawHeight = attrAs<int32_t>(options, "overlayRawHeight");
baton->overlayRawChannels = attrAs<int32_t>(options, "overlayRawChannels");
}
baton->overlayGravity = attrAs<int32_t>(options, "overlayGravity");
baton->overlayXOffset = attrAs<int32_t>(options, "overlayXOffset");
Expand All @@ -1230,6 +1269,9 @@ NAN_METHOD(pipeline) {
baton->booleanBufferInLength = node::Buffer::Length(booleanBufferIn);
baton->booleanBufferIn = node::Buffer::Data(booleanBufferIn);
buffersToPersist.push_back(booleanBufferIn);
baton->booleanRawWidth = attrAs<int32_t>(options, "booleanRawWidth");
baton->booleanRawHeight = attrAs<int32_t>(options, "booleanRawHeight");
baton->booleanRawChannels = attrAs<int32_t>(options, "booleanRawChannels");
}
// Resize options
baton->withoutEnlargement = attrAs<bool>(options, "withoutEnlargement");
Expand Down
12 changes: 12 additions & 0 deletions src/pipeline.h
Original file line number Diff line number Diff line change
Expand Up @@ -39,9 +39,15 @@ struct PipelineBaton {
int overlayYOffset;
bool overlayTile;
bool overlayCutout;
int overlayRawWidth;
int overlayRawHeight;
int overlayRawChannels;
std::string booleanFileIn;
char *booleanBufferIn;
size_t booleanBufferInLength;
int booleanRawWidth;
int booleanRawHeight;
int booleanRawChannels;
int topOffsetPre;
int leftOffsetPre;
int widthPre;
Expand Down Expand Up @@ -120,7 +126,13 @@ struct PipelineBaton {
overlayYOffset(-1),
overlayTile(false),
overlayCutout(false),
overlayRawWidth(0),
overlayRawHeight(0),
overlayRawChannels(0),
booleanBufferInLength(0),
booleanRawWidth(0),
booleanRawHeight(0),
booleanRawChannels(0),
topOffsetPre(-1),
topOffsetPost(-1),
channels(0),
Expand Down
22 changes: 22 additions & 0 deletions test/unit/boolean.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,22 @@ describe('Boolean operation between two images', function() {
fixtures.assertSimilar(fixtures.expected('boolean_' + op + '_result.jpg'), data, done);
});
});
});

it('Raw buffer input', function(done) {
sharp(fixtures.inputJpgBooleanTest).raw().toBuffer(
function(err, buf, info) {
if (err) throw err;
sharp(fixtures.inputJpg)
.resize(320, 240)
.boolean(buf, 'and', {raw: info})
.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('Invalid operation', function() {
Expand All @@ -59,4 +74,11 @@ describe('Boolean operation between two images', function() {
sharp().boolean();
});
});

it('Invalid raw buffer description', function() {
assert.throws(function() {
sharp().boolean(fs.readFileSync(fixtures.inputJpg),{raw:{}});
});
});

});
26 changes: 26 additions & 0 deletions test/unit/overlay.js
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,26 @@ describe('Overlays', function() {
});
});

it('Composite rgb+alpha raw buffer onto JPEG', function(done) {
var paths = getPaths('overlay-jpeg-with-rgb', 'jpg');

sharp(fixtures.inputPngOverlayLayer1)
.raw()
.toBuffer(function(err, buf, info) {
if (err) throw err;

sharp(fixtures.inputJpg)
.resize(2048, 1536)
.overlayWith(buf, {raw:info})
.toFile(paths.actual, function(error, info) {
if (error) return done(error);
fixtures.assertMaxColourDistance(paths.actual, paths.expected, 102);
done();
});
});
});


it('Composite greyscale+alpha PNG onto JPEG', function(done) {
var paths = getPaths('overlay-jpeg-with-greyscale', 'jpg');

Expand Down Expand Up @@ -528,4 +548,10 @@ describe('Overlays', function() {
});
});

it('Fail with invalid raw buffer description', function() {
assert.throws(function() {
sharp().overlayWith(fs.readFileSyc(fixtures.inputJpg),{raw:{}});
});
});

});