diff --git a/docs/api-operation.md b/docs/api-operation.md
index c4a69cdce..5abc6eed4 100644
--- a/docs/api-operation.md
+++ b/docs/api-operation.md
@@ -580,7 +580,7 @@ Recombine the image with the specified matrix.
| Param | Type | Description |
| --- | --- | --- |
-| inputMatrix | Array.<Array.<number>>
| 3x3 Recombination matrix |
+| inputMatrix | Array.<Array.<number>>
| 3x3 or 4x4 Recombination matrix |
**Example**
```js
diff --git a/docs/changelog.md b/docs/changelog.md
index 6af2668e8..3850f50ab 100644
--- a/docs/changelog.md
+++ b/docs/changelog.md
@@ -16,6 +16,10 @@ Requires libvips v8.15.2
* Ensure `sharp.format.heif` includes only AVIF when using prebuilt binaries.
[#4132](https://github.com/lovell/sharp/issues/4132)
+* Add support to recomb operation for 4x4 matrices.
+ [#4147](https://github.com/lovell/sharp/pull/4147)
+ [@ton11797](https://github.com/ton11797)
+
### v0.33.4 - 16th May 2024
* Remove experimental status from `pipelineColourspace`.
diff --git a/docs/humans.txt b/docs/humans.txt
index 20a9fa8e9..1c9cd1ceb 100644
--- a/docs/humans.txt
+++ b/docs/humans.txt
@@ -296,3 +296,6 @@ GitHub: https://github.com/adriaanmeuris
Name: Richard Hillmann
GitHub: https://github.com/project0
+
+Name: Pongsatorn Manusopit
+GitHub: https://github.com/ton11797
diff --git a/lib/index.d.ts b/lib/index.d.ts
index 4dbf3b8d7..6e25529d8 100644
--- a/lib/index.d.ts
+++ b/lib/index.d.ts
@@ -571,11 +571,11 @@ declare namespace sharp {
/**
* Recomb the image with the specified matrix.
- * @param inputMatrix 3x3 Recombination matrix
+ * @param inputMatrix 3x3 Recombination matrix or 4x4 Recombination matrix
* @throws {Error} Invalid parameters
* @returns A sharp instance that can be used to chain operations
*/
- recomb(inputMatrix: Matrix3x3): Sharp;
+ recomb(inputMatrix: Matrix3x3 | Matrix4x4): Sharp;
/**
* Transforms the image using brightness, saturation, hue rotation and lightness.
@@ -1730,6 +1730,7 @@ declare namespace sharp {
type Matrix2x2 = [[number, number], [number, number]];
type Matrix3x3 = [[number, number, number], [number, number, number], [number, number, number]];
+ type Matrix4x4 = [[number, number, number, number], [number, number, number, number], [number, number, number, number], [number, number, number, number]];
}
export = sharp;
diff --git a/lib/operation.js b/lib/operation.js
index ed6df8345..bac9c8891 100644
--- a/lib/operation.js
+++ b/lib/operation.js
@@ -787,24 +787,22 @@ function linear (a, b) {
* // With this example input, a sepia filter has been applied
* });
*
- * @param {Array>} inputMatrix - 3x3 Recombination matrix
+ * @param {Array>} inputMatrix - 3x3 or 4x4 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 recombination matrix');
+ if (!Array.isArray(inputMatrix)) {
+ throw is.invalidParameterError('inputMatrix', 'array', inputMatrix);
+ }
+ if (inputMatrix.length !== 3 && inputMatrix.length !== 4) {
+ throw is.invalidParameterError('inputMatrix', '3x3 or 4x4 array', inputMatrix.length);
+ }
+ const recombMatrix = inputMatrix.flat().map(Number);
+ if (recombMatrix.length !== 9 && recombMatrix.length !== 16) {
+ throw is.invalidParameterError('inputMatrix', 'cardinality of 9 or 16', recombMatrix.length);
}
- 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);
+ this.options.recombMatrix = recombMatrix;
return this;
}
diff --git a/src/operations.cc b/src/operations.cc
index c6904c50d..57790c001 100644
--- a/src/operations.cc
+++ b/src/operations.cc
@@ -183,19 +183,21 @@ namespace sharp {
* 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();
+ VImage Recomb(VImage image, std::vector const& matrix) {
+ double* m = const_cast(matrix.data());
image = image.colourspace(VIPS_INTERPRETATION_sRGB);
- return image
- .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));
+ if (matrix.size() == 9) {
+ return image
+ .recomb(image.bands() == 3
+ ? VImage::new_matrix(3, 3, m, 9)
+ : 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));
+ } else {
+ return image.recomb(VImage::new_matrix(4, 4, m, 16));
+ }
}
VImage Modulate(VImage image, double const brightness, double const saturation,
diff --git a/src/operations.h b/src/operations.h
index f2d73704a..8c8791c6b 100644
--- a/src/operations.h
+++ b/src/operations.h
@@ -95,7 +95,7 @@ namespace sharp {
* 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);
+ VImage Recomb(VImage image, std::vector const &matrix);
/*
* Modulate brightness, saturation, hue and lightness
diff --git a/src/pipeline.cc b/src/pipeline.cc
index 9dc22ed72..1455c5751 100644
--- a/src/pipeline.cc
+++ b/src/pipeline.cc
@@ -609,7 +609,7 @@ class PipelineWorker : public Napi::AsyncWorker {
}
// Recomb
- if (baton->recombMatrix != NULL) {
+ if (!baton->recombMatrix.empty()) {
image = sharp::Recomb(image, baton->recombMatrix);
}
@@ -1613,10 +1613,11 @@ Napi::Value pipeline(const Napi::CallbackInfo& info) {
}
}
if (options.Has("recombMatrix")) {
- baton->recombMatrix = std::unique_ptr(new double[9]);
Napi::Array recombMatrix = options.Get("recombMatrix").As();
- for (unsigned int i = 0; i < 9; i++) {
- baton->recombMatrix[i] = sharp::AttrAsDouble(recombMatrix, i);
+ unsigned int matrixElements = recombMatrix.Length();
+ baton->recombMatrix.resize(matrixElements);
+ for (unsigned int i = 0; i < matrixElements; i++) {
+ baton->recombMatrix[i] = sharp::AttrAsDouble(recombMatrix, i);
}
}
baton->colourspacePipeline = sharp::AttrAsEnum(
diff --git a/src/pipeline.h b/src/pipeline.h
index bc79eb2cc..163f84f1c 100644
--- a/src/pipeline.h
+++ b/src/pipeline.h
@@ -223,7 +223,7 @@ struct PipelineBaton {
VipsForeignDzDepth tileDepth;
std::string tileId;
std::string tileBasename;
- std::unique_ptr recombMatrix;
+ std::vector recombMatrix;
PipelineBaton():
input(nullptr),
diff --git a/test/fixtures/d.png b/test/fixtures/d.png
new file mode 100644
index 000000000..765420660
Binary files /dev/null and b/test/fixtures/d.png differ
diff --git a/test/fixtures/expected/d-opacity-30.png b/test/fixtures/expected/d-opacity-30.png
new file mode 100644
index 000000000..d053cfa2b
Binary files /dev/null and b/test/fixtures/expected/d-opacity-30.png differ
diff --git a/test/fixtures/index.js b/test/fixtures/index.js
index 40f05dbc9..c1fe8b29c 100644
--- a/test/fixtures/index.js
+++ b/test/fixtures/index.js
@@ -139,6 +139,7 @@ module.exports = {
testPattern: getPath('test-pattern.png'),
+ inputPngWithTransparent: getPath('d.png'),
// Path for tests requiring human inspection
path: getPath,
diff --git a/test/types/sharp.test-d.ts b/test/types/sharp.test-d.ts
index 0a6c9a3ef..e5eea4427 100644
--- a/test/types/sharp.test-d.ts
+++ b/test/types/sharp.test-d.ts
@@ -295,6 +295,13 @@ sharp('input.gif')
[0.2392, 0.4696, 0.0912],
])
+ .recomb([
+ [1,0,0,0],
+ [0,1,0,0],
+ [0,0,1,0],
+ [0,0,0,1],
+ ])
+
.modulate({ brightness: 2 })
.modulate({ hue: 180 })
.modulate({ lightness: 10 })
diff --git a/test/unit/recomb.js b/test/unit/recomb.js
index 9b000e6ff..499597204 100644
--- a/test/unit/recomb.js
+++ b/test/unit/recomb.js
@@ -121,6 +121,29 @@ describe('Recomb', function () {
});
});
+ it('applies opacity 30% to the image', function (done) {
+ const output = fixtures.path('output.recomb-opacity.png');
+ sharp(fixtures.inputPngWithTransparent)
+ .recomb([
+ [1, 0, 0, 0],
+ [0, 1, 0, 0],
+ [0, 0, 1, 0],
+ [0, 0, 0, 0.3]
+ ])
+ .toFile(output, function (err, info) {
+ if (err) throw err;
+ assert.strictEqual('png', info.format);
+ assert.strictEqual(48, info.width);
+ assert.strictEqual(48, info.height);
+ fixtures.assertMaxColourDistance(
+ output,
+ fixtures.expected('d-opacity-30.png'),
+ 17
+ );
+ done();
+ });
+ });
+
describe('invalid matrix specification', function () {
it('missing', function () {
assert.throws(function () {