diff --git a/docs/api-operation.md b/docs/api-operation.md
index cfe05a53..44822543 100644
--- a/docs/api-operation.md
+++ b/docs/api-operation.md
@@ -254,6 +254,35 @@ Apply the linear formula a \* input + b to the image (levels adjustment)
- `b` **[Number][1]** offset (optional, default `0.0`)
+- Throws **[Error][5]** Invalid parameters
+
+Returns **Sharp**
+
+## recomb
+
+Recomb the image with the specified matrix.
+
+### Parameters
+
+- `inputMatrix`
+- `3x3` **[Array][7]<[Array][7]<[Number][1]>>** Recombination matrix
+
+### Examples
+
+```javascript
+sharp(input)
+ .recomb([
+ [0.3588, 0.7044, 0.1368],
+ [0.2990, 0.5870, 0.1140],
+ [0.2392, 0.4696, 0.0912],
+ ])
+ .raw()
+ .toBuffer(function(err, data, info) {
+ // data contains the raw pixel data after applying the recomb
+ // With this example input, a sepia filter has been applied
+ });
+```
+
- Throws **[Error][5]** Invalid parameters
Returns **Sharp**
diff --git a/docs/changelog.md b/docs/changelog.md
index 36bb6bf3..ecbaf25a 100644
--- a/docs/changelog.md
+++ b/docs/changelog.md
@@ -25,6 +25,10 @@ Requires libvips v8.7.0.
[#1475](https://github.com/lovell/sharp/pull/1475)
[@jaubourg](https://github.com/jaubourg)
+* Expose libvips' recombination matrix operation.
+ [#1477](https://github.com/lovell/sharp/pull/1477)
+ [@fromkeith](https://github.com/fromkeith)
+
#### v0.21.0 - 4th October 2018
* Deprecate the following resize-related functions:
diff --git a/docs/index.md b/docs/index.md
index 991680fb..d35fb450 100644
--- a/docs/index.md
+++ b/docs/index.md
@@ -120,6 +120,7 @@ the help and code contributions of the following people:
* [Axel Eirola](https://github.com/aeirola)
* [Freezy](https://github.com/freezy)
* [Julian Aubourg](https://github.com/jaubourg)
+* [Keith Belovay](https://github.com/fromkeith)
Thank you!
diff --git a/lib/operation.js b/lib/operation.js
index 80cd3480..82be10e6 100644
--- a/lib/operation.js
+++ b/lib/operation.js
@@ -378,6 +378,43 @@ function linear (a, b) {
return this;
}
+/**
+ * Recomb the image with the specified matrix.
+ *
+ * @example
+ * sharp(input)
+ * .recomb([
+ * [0.3588, 0.7044, 0.1368],
+ * [0.2990, 0.5870, 0.1140],
+ * [0.2392, 0.4696, 0.0912],
+ * ])
+ * .raw()
+ * .toBuffer(function(err, data, info) {
+ * // data contains the raw pixel data after applying the recomb
+ * // With this example input, a sepia filter has been applied
+ * });
+ *
+ * @param {Array>} 3x3 Recombination matrix
+ * @returns {Sharp}
+ * @throws {Error} Invalid parameters
+ */
+function recomb (inputMatrix) {
+ if (!Array.isArray(inputMatrix) || inputMatrix.length !== 3 ||
+ inputMatrix[0].length !== 3 ||
+ inputMatrix[1].length !== 3 ||
+ inputMatrix[2].length !== 3
+ ) {
+ // must pass in a kernel
+ throw new Error('Invalid Recomb Matrix');
+ }
+ this.options.recombMatrix = [
+ inputMatrix[0][0], inputMatrix[0][1], inputMatrix[0][2],
+ inputMatrix[1][0], inputMatrix[1][1], inputMatrix[1][2],
+ inputMatrix[2][0], inputMatrix[2][1], inputMatrix[2][2]
+ ].map(Number);
+ return this;
+}
+
/**
* Decorate the Sharp prototype with operation-related functions.
* @private
@@ -398,6 +435,7 @@ module.exports = function (Sharp) {
convolve,
threshold,
boolean,
- linear
+ linear,
+ recomb
});
};
diff --git a/package.json b/package.json
index 485482c5..9d81d884 100644
--- a/package.json
+++ b/package.json
@@ -57,7 +57,8 @@
"Axel Eirola ",
"Freezy ",
"Daiz ",
- "Julian Aubourg "
+ "Julian Aubourg ",
+ "Keith Belovay "
],
"scripts": {
"install": "(node install/libvips && node install/dll-copy && prebuild-install) || (node-gyp rebuild && node install/dll-copy)",
diff --git a/src/operations.cc b/src/operations.cc
index f389b33e..71634644 100644
--- a/src/operations.cc
+++ b/src/operations.cc
@@ -278,6 +278,25 @@ namespace sharp {
return image.conv(kernel);
}
+ /*
+ * Recomb with a Matrix of the given bands/channel size.
+ * Eg. RGB will be a 3x3 matrix.
+ */
+ VImage Recomb(VImage image, std::unique_ptr const &matrix) {
+ double *m = matrix.get();
+ return image
+ .colourspace(VIPS_INTERPRETATION_sRGB)
+ .recomb(image.bands() == 3
+ ? VImage::new_from_memory(
+ m, 9 * sizeof(double), 3, 3, 1, VIPS_FORMAT_DOUBLE
+ )
+ : VImage::new_matrixv(4, 4,
+ m[0], m[1], m[2], 0.0,
+ m[3], m[4], m[5], 0.0,
+ m[6], m[7], m[8], 0.0,
+ 0.0, 0.0, 0.0, 1.0));
+ }
+
/*
* Sharpen flat and jagged areas. Use sigma of -1.0 for fast sharpen.
*/
diff --git a/src/operations.h b/src/operations.h
index ee371a3f..20cc748a 100644
--- a/src/operations.h
+++ b/src/operations.h
@@ -107,6 +107,12 @@ namespace sharp {
*/
VImage Linear(VImage image, double const a, double const b);
+ /*
+ * Recomb with a Matrix of the given bands/channel size.
+ * Eg. RGB will be a 3x3 matrix.
+ */
+ VImage Recomb(VImage image, std::unique_ptr const &matrix);
+
} // namespace sharp
#endif // SRC_OPERATIONS_H_
diff --git a/src/pipeline.cc b/src/pipeline.cc
index e5fcf87b..6258b11a 100644
--- a/src/pipeline.cc
+++ b/src/pipeline.cc
@@ -525,6 +525,11 @@ class PipelineWorker : public Nan::AsyncWorker {
baton->convKernel);
}
+ // Recomb
+ if (baton->recombMatrix != NULL) {
+ image = sharp::Recomb(image, baton->recombMatrix);
+ }
+
// Sharpen
if (shouldSharpen) {
image = sharp::Sharpen(image, baton->sharpenSigma, baton->sharpenFlat, baton->sharpenJagged);
@@ -1234,6 +1239,13 @@ NAN_METHOD(pipeline) {
baton->convKernel[i] = AttrTo(kdata, i);
}
}
+ if (HasAttr(options, "recombMatrix")) {
+ baton->recombMatrix = std::unique_ptr(new double[9]);
+ v8::Local recombMatrix = AttrAs(options, "recombMatrix");
+ for (unsigned int i = 0; i < 9; i++) {
+ baton->recombMatrix[i] = AttrTo(recombMatrix, i);
+ }
+ }
baton->colourspace = sharp::GetInterpretation(AttrAsStr(options, "colourspace"));
if (baton->colourspace == VIPS_INTERPRETATION_ERROR) {
baton->colourspace = VIPS_INTERPRETATION_sRGB;
diff --git a/src/pipeline.h b/src/pipeline.h
index fb18ab2b..5e020a57 100644
--- a/src/pipeline.h
+++ b/src/pipeline.h
@@ -146,6 +146,7 @@ struct PipelineBaton {
std::string tileFormat;
int tileAngle;
VipsForeignDzDepth tileDepth;
+ std::unique_ptr recombMatrix;
PipelineBaton():
input(nullptr),
diff --git a/test/fixtures/expected/Landscape_1-recomb-saturation.jpg b/test/fixtures/expected/Landscape_1-recomb-saturation.jpg
new file mode 100644
index 00000000..88810283
Binary files /dev/null and b/test/fixtures/expected/Landscape_1-recomb-saturation.jpg differ
diff --git a/test/fixtures/expected/Landscape_1-recomb-sepia.jpg b/test/fixtures/expected/Landscape_1-recomb-sepia.jpg
new file mode 100644
index 00000000..9ca75b63
Binary files /dev/null and b/test/fixtures/expected/Landscape_1-recomb-sepia.jpg differ
diff --git a/test/fixtures/expected/Landscape_1-recomb-sepia2.jpg b/test/fixtures/expected/Landscape_1-recomb-sepia2.jpg
new file mode 100644
index 00000000..7d0f1996
Binary files /dev/null and b/test/fixtures/expected/Landscape_1-recomb-sepia2.jpg differ
diff --git a/test/fixtures/expected/alpha-recomb-sepia.png b/test/fixtures/expected/alpha-recomb-sepia.png
new file mode 100644
index 00000000..6e3daf9c
Binary files /dev/null and b/test/fixtures/expected/alpha-recomb-sepia.png differ
diff --git a/test/unit/recomb.js b/test/unit/recomb.js
new file mode 100644
index 00000000..b9f889e9
--- /dev/null
+++ b/test/unit/recomb.js
@@ -0,0 +1,131 @@
+'use strict';
+
+const assert = require('assert');
+
+const sharp = require('../../');
+const fixtures = require('../fixtures');
+
+describe('Recomb', function () {
+ it('applies a sepia filter using recomb', function (done) {
+ const output = fixtures.path('output.recomb-sepia.jpg');
+ sharp(fixtures.inputJpgWithLandscapeExif1)
+ .recomb([
+ [0.3588, 0.7044, 0.1368],
+ [0.299, 0.587, 0.114],
+ [0.2392, 0.4696, 0.0912]
+ ])
+ .toFile(output, function (err, info) {
+ if (err) throw err;
+ assert.strictEqual('jpeg', info.format);
+ assert.strictEqual(600, info.width);
+ assert.strictEqual(450, info.height);
+ fixtures.assertMaxColourDistance(
+ output,
+ fixtures.expected('Landscape_1-recomb-sepia.jpg')
+ );
+ done();
+ });
+ });
+
+ it('applies a sepia filter using recomb to an PNG with Alpha', function (done) {
+ const output = fixtures.path('output.recomb-sepia.png');
+ sharp(fixtures.inputPngAlphaPremultiplicationSmall)
+ .recomb([
+ [0.3588, 0.7044, 0.1368],
+ [0.299, 0.587, 0.114],
+ [0.2392, 0.4696, 0.0912]
+ ])
+ .toFile(output, function (err, info) {
+ if (err) throw err;
+ assert.strictEqual('png', info.format);
+ assert.strictEqual(1024, info.width);
+ assert.strictEqual(768, info.height);
+ fixtures.assertMaxColourDistance(
+ output,
+ fixtures.expected('alpha-recomb-sepia.png')
+ );
+ done();
+ });
+ });
+
+ it('applies a different sepia filter using recomb', function (done) {
+ const output = fixtures.path('output.recomb-sepia2.jpg');
+ sharp(fixtures.inputJpgWithLandscapeExif1)
+ .recomb([
+ [0.393, 0.769, 0.189],
+ [0.349, 0.686, 0.168],
+ [0.272, 0.534, 0.131]
+ ])
+ .toFile(output, function (err, info) {
+ if (err) throw err;
+ assert.strictEqual('jpeg', info.format);
+ assert.strictEqual(600, info.width);
+ assert.strictEqual(450, info.height);
+ fixtures.assertMaxColourDistance(
+ output,
+ fixtures.expected('Landscape_1-recomb-sepia2.jpg')
+ );
+ done();
+ });
+ });
+ it('increases the saturation of the image', function (done) {
+ const saturationLevel = 1;
+ const output = fixtures.path('output.recomb-saturation.jpg');
+ sharp(fixtures.inputJpgWithLandscapeExif1)
+ .recomb([
+ [
+ saturationLevel + 1 - 0.2989,
+ -0.587 * saturationLevel,
+ -0.114 * saturationLevel
+ ],
+ [
+ -0.2989 * saturationLevel,
+ saturationLevel + 1 - 0.587,
+ -0.114 * saturationLevel
+ ],
+ [
+ -0.2989 * saturationLevel,
+ -0.587 * saturationLevel,
+ saturationLevel + 1 - 0.114
+ ]
+ ])
+ .toFile(output, function (err, info) {
+ if (err) throw err;
+ assert.strictEqual('jpeg', info.format);
+ assert.strictEqual(600, info.width);
+ assert.strictEqual(450, info.height);
+ fixtures.assertMaxColourDistance(
+ output,
+ fixtures.expected('Landscape_1-recomb-saturation.jpg')
+ );
+ done();
+ });
+ });
+
+ describe('invalid matrix specification', function () {
+ it('missing', function () {
+ assert.throws(function () {
+ sharp(fixtures.inputJpg).recomb();
+ });
+ });
+ it('incorrect flat data', function () {
+ assert.throws(function () {
+ sharp(fixtures.inputJpg).recomb([1, 2, 3, 4, 5, 6, 7, 8, 9]);
+ });
+ });
+ it('incorrect sub size', function () {
+ assert.throws(function () {
+ sharp(fixtures.inputJpg).recomb([
+ [1, 2, 3, 4],
+ [5, 6, 7, 8],
+ [1, 2, 9, 6]
+ ]);
+ });
+ });
+ it('incorrect top size', function () {
+ assert.throws(function () {
+ sharp(fixtures.inputJpg).recomb([[1, 2, 3, 4], [5, 6, 7, 8]]);
+ });
+ });
+ });
+});