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 () {