From cacc3922456e0d9f1401db8addef557b3d0b0b89 Mon Sep 17 00:00:00 2001 From: Matt Hirsch Date: Tue, 19 Jul 2016 01:33:16 -0400 Subject: [PATCH 1/3] boolean() and overlayWith() accept raw buffers as input, described using same syntax as sharp() constructor --- docs/api.md | 9 ++++-- index.js | 43 ++++++++++++++++++++++++- src/pipeline.cc | 74 ++++++++++++++++++++++++++++++++++---------- src/pipeline.h | 12 +++++++ test/unit/boolean.js | 22 +++++++++++++ test/unit/overlay.js | 26 ++++++++++++++++ 6 files changed, 166 insertions(+), 20 deletions(-) diff --git a/docs/api.md b/docs/api.md index 0d872b526..76077c9d5 100644 --- a/docs/api.md +++ b/docs/api.md @@ -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: @@ -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`. @@ -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. @@ -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]) diff --git a/index.js b/index.js index 941f7e191..47dc823ea 100644 --- a/index.js +++ b/index.js @@ -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, @@ -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: '', @@ -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)) { @@ -382,6 +389,23 @@ 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; }; @@ -389,6 +413,8 @@ Sharp.prototype.boolean = function(operand, operator) { Overlay with another image, using an optional gravity */ Sharp.prototype.overlayWith = function(overlay, options) { + /* jshint maxcomplexity:16 */ + if (isString(overlay)) { this.options.overlayFileIn = overlay; } else if (isBuffer(overlay)) { @@ -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; }; diff --git a/src/pipeline.cc b/src/pipeline.cc index 782237500..ed59735ac 100644 --- a/src/pipeline.cc +++ b/src/pipeline.cc @@ -670,18 +670,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 @@ -781,17 +798,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->rawChannels < 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 @@ -849,6 +884,7 @@ class PipelineWorker : public AsyncWorker { baton->channels = image.bands(); baton->width = image.width(); baton->height = image.height(); + // Output if (baton->fileOut == "") { // Buffer output @@ -1197,6 +1233,9 @@ NAN_METHOD(pipeline) { baton->overlayBufferInLength = node::Buffer::Length(overlayBufferIn); baton->overlayBufferIn = node::Buffer::Data(overlayBufferIn); buffersToPersist.push_back(overlayBufferIn); + baton->overlayRawWidth = attrAs(options, "overlayRawWidth"); + baton->overlayRawHeight = attrAs(options, "overlayRawHeight"); + baton->overlayRawChannels = attrAs(options, "overlayRawChannels"); } baton->overlayGravity = attrAs(options, "overlayGravity"); baton->overlayXOffset = attrAs(options, "overlayXOffset"); @@ -1211,6 +1250,9 @@ NAN_METHOD(pipeline) { baton->booleanBufferInLength = node::Buffer::Length(booleanBufferIn); baton->booleanBufferIn = node::Buffer::Data(booleanBufferIn); buffersToPersist.push_back(booleanBufferIn); + baton->booleanRawWidth = attrAs(options, "booleanRawWidth"); + baton->booleanRawHeight = attrAs(options, "booleanRawHeight"); + baton->booleanRawChannels = attrAs(options, "booleanRawChannels"); } // Resize options baton->withoutEnlargement = attrAs(options, "withoutEnlargement"); diff --git a/src/pipeline.h b/src/pipeline.h index 68961fd5d..88ec754b1 100644 --- a/src/pipeline.h +++ b/src/pipeline.h @@ -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; @@ -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), diff --git a/test/unit/boolean.js b/test/unit/boolean.js index eae650955..5eb976e69 100644 --- a/test/unit/boolean.js +++ b/test/unit/boolean.js @@ -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() { @@ -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:{}}); + }); + }); + }); diff --git a/test/unit/overlay.js b/test/unit/overlay.js index 4b8d6f70a..cb5d9aa91 100644 --- a/test/unit/overlay.js +++ b/test/unit/overlay.js @@ -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'); @@ -528,4 +548,10 @@ describe('Overlays', function() { }); }); + if('Fail with invalid raw buffer description', function() { + assert.throws(function() { + sharp().overlayWith(fs.readFileSyc(fixtures.inputJpg),{raw:{}}); + }); + }); + }); From 71d8f985afeda1fd50e5a42d823c88fe9a7c353f Mon Sep 17 00:00:00 2001 From: Matt Hirsch Date: Wed, 20 Jul 2016 15:39:20 -0400 Subject: [PATCH 2/3] used the wrong rawchannels variable for boolean input --- src/pipeline.cc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pipeline.cc b/src/pipeline.cc index ed59735ac..ba2cda031 100644 --- a/src/pipeline.cc +++ b/src/pipeline.cc @@ -803,7 +803,7 @@ class PipelineWorker : public AsyncWorker { try { booleanImage = VImage::new_from_memory(baton->booleanBufferIn, baton->booleanBufferInLength, baton->booleanRawWidth, baton->booleanRawHeight, baton->booleanRawChannels, VIPS_FORMAT_UCHAR); - if (baton->rawChannels < 3) { + if (baton->booleanRawChannels < 3) { booleanImage.get_image()->Type = VIPS_INTERPRETATION_B_W; } else { booleanImage.get_image()->Type = VIPS_INTERPRETATION_sRGB; From 99f84529ea8670a8838ae0cb860ed2f4dde3944a Mon Sep 17 00:00:00 2001 From: Matt Hirsch Date: Wed, 20 Jul 2016 19:07:54 -0400 Subject: [PATCH 3/3] typo in unit test --- test/unit/overlay.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/unit/overlay.js b/test/unit/overlay.js index cb5d9aa91..6534f41a5 100644 --- a/test/unit/overlay.js +++ b/test/unit/overlay.js @@ -548,7 +548,7 @@ describe('Overlays', function() { }); }); - if('Fail with invalid raw buffer description', function() { + it('Fail with invalid raw buffer description', function() { assert.throws(function() { sharp().overlayWith(fs.readFileSyc(fixtures.inputJpg),{raw:{}}); });