diff --git a/lib/constructor.js b/lib/constructor.js index f6741509b..66d814503 100644 --- a/lib/constructor.js +++ b/lib/constructor.js @@ -346,6 +346,7 @@ const Sharp = function (input, options) { timeoutSeconds: 0, linearA: [], linearB: [], + // Function to notify of libvips warnings debuglog: warning => { this.emit('warning', warning); 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..6f0fcd098 100644 --- a/lib/operation.js +++ b/lib/operation.js @@ -787,24 +787,23 @@ function linear (a, b) { * // With this example input, a sepia filter has been applied * }); * - * @param {Array>} inputMatrix - 3x3 Recombination matrix + * @param {Array>} inputMatrix - 3x3 Recombination matrix 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 + if (!Array.isArray(inputMatrix)) { + throw new Error('Invalid recombination matrix'); + } + if (inputMatrix.length !== 3 && inputMatrix.length !== 4) { throw new Error('Invalid recombination 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); + const recombMatrix = inputMatrix.flat().map(Number); + if (recombMatrix.length !== 9 && recombMatrix.length !== 16) { + throw new Error('Invalid recombination matrix'); + } + + this.options.recombMatrix = recombMatrix; return this; } diff --git a/src/operations.cc b/src/operations.cc index c6904c50d..f41fe1093 100644 --- a/src/operations.cc +++ b/src/operations.cc @@ -183,19 +183,25 @@ 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) { + const double* m = matrix.data(); + void *matrix_data = const_cast(static_cast(m)); 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_from_memory( + matrix_data, 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)); + } else { + return image + .recomb(VImage::new_from_memory(matrix_data, 16 * sizeof(double), 4, 4, 1, VIPS_FORMAT_DOUBLE)); + } } 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 () {