diff --git a/docs/api-operation.md b/docs/api-operation.md index d38e209ff..05851895d 100644 --- a/docs/api-operation.md +++ b/docs/api-operation.md @@ -265,6 +265,30 @@ const gaussianBlurred = await sharp(input) .toBuffer(); ``` +## dilate + +Dilate the image. + +**Throws**: + +- Error Invalid parameters + +| Param | Type | Default | Description | +| --- | --- | --- | --- | +| [width] | number | 1 | a value between 1 and XX representing the mask size | + +## erode + +Erodes the image. + +**Throws**: + +- Error Invalid parameters + +| Param | Type | Default | Description | +| --- | --- | --- | --- | +| [width] | number | 1 | a value between 1 and XX representing the mask size | + ## flatten > flatten([options]) ⇒ Sharp diff --git a/lib/constructor.js b/lib/constructor.js index 0ff85c667..b66bff2e6 100644 --- a/lib/constructor.js +++ b/lib/constructor.js @@ -240,6 +240,8 @@ const Sharp = function (input, options) { trimBackground: [], trimThreshold: -1, trimLineArt: false, + dilateWidth: 0, + erodeWidth: 0, gamma: 0, gammaOut: 0, greyscale: false, diff --git a/lib/operation.js b/lib/operation.js index 538286491..936ab289f 100644 --- a/lib/operation.js +++ b/lib/operation.js @@ -426,6 +426,44 @@ function blur (options) { return this; } +/** + * Dilate the image. + * @param {Number} [width] dilate width. + * @returns {Sharp} + * @throws {Error} Invalid parameters + */ +function dilate (width) { + if (!is.defined(width)) { + // No arguments: default to 1px dilation (3x3 mask) + this.options.dilateWidth = 1; + } else if (is.integer(width) && width > 0) { + // Numeric argument: specific width + this.options.dilateWidth = width; + } else { + throw is.invalidParameterError('dilate', 'positive integer', dilate); + } + return this; +} + +/** + * Erode the image. + * @param {Number} [width] erode width. + * @returns {Sharp} + * @throws {Error} Invalid parameters + */ +function erode (width) { + if (!is.defined(width)) { + // No arguments: default to 1px erosion (3x3 mask) + this.options.erodeWidth = 1; + } else if (is.integer(width) && width > 0) { + // Numeric argument: specific width + this.options.erodeWidth = width; + } else { + throw is.invalidParameterError('erode', 'positive integer', erode); + } + return this; +} + /** * Merge alpha transparency channel, if any, with a background, then remove the alpha channel. * @@ -940,6 +978,8 @@ module.exports = function (Sharp) { flop, affine, sharpen, + erode, + dilate, median, blur, flatten, diff --git a/src/operations.cc b/src/operations.cc index 775f6134e..071ee6592 100644 --- a/src/operations.cc +++ b/src/operations.cc @@ -472,4 +472,28 @@ namespace sharp { } } + /* + * Dilate an image + */ + VImage Dilate(VImage image, int const width) { + const int maskWidth = 2*width + 1; + VImage mask = VImage::new_matrix(maskWidth, maskWidth); + + return image.morph( + mask, + VIPS_OPERATION_MORPHOLOGY_DILATE).invert(); + } + + /* + * Erode an image + */ + VImage Erode(VImage image, int const width) { + const int maskWidth = 2*width + 1; + VImage mask = VImage::new_matrix(maskWidth, maskWidth); + + return image.morph( + mask, + VIPS_OPERATION_MORPHOLOGY_ERODE).invert(); + } + } // namespace sharp diff --git a/src/operations.h b/src/operations.h index b2881d65d..22ff46fcf 100644 --- a/src/operations.h +++ b/src/operations.h @@ -120,6 +120,15 @@ namespace sharp { VImage EmbedMultiPage(VImage image, int left, int top, int width, int height, VipsExtend extendWith, std::vector background, int nPages, int *pageHeight); + /* + * Dilate an image + */ + VImage Dilate(VImage image, int const maskWidth); + + /* + * Erode an image + */ + VImage Erode(VImage image, int const maskWidth); } // namespace sharp #endif // SRC_OPERATIONS_H_ diff --git a/src/pipeline.cc b/src/pipeline.cc index ac5ead01e..cfe72937a 100644 --- a/src/pipeline.cc +++ b/src/pipeline.cc @@ -591,6 +591,16 @@ class PipelineWorker : public Napi::AsyncWorker { image = sharp::Threshold(image, baton->threshold, baton->thresholdGrayscale); } + // Dilate - must happen before blurring, due to the utility of dilating after thresholding + if (baton->dilateWidth != 0) { + image = sharp::Dilate(image, baton->dilateWidth); + } + + // Erode - must happen before blurring, due to the utility of eroding after thresholding + if (baton->erodeWidth != 0) { + image = sharp::Erode(image, baton->erodeWidth); + } + // Blur if (shouldBlur) { image = sharp::Blur(image, baton->blurSigma, baton->precision, baton->minAmpl); @@ -1564,6 +1574,8 @@ Napi::Value pipeline(const Napi::CallbackInfo& info) { baton->gammaOut = sharp::AttrAsDouble(options, "gammaOut"); baton->linearA = sharp::AttrAsVectorOfDouble(options, "linearA"); baton->linearB = sharp::AttrAsVectorOfDouble(options, "linearB"); + baton->dilateWidth = sharp::AttrAsUint32(options, "dilateWidth"); + baton->erodeWidth = sharp::AttrAsUint32(options, "erodeWidth"); baton->greyscale = sharp::AttrAsBool(options, "greyscale"); baton->normalise = sharp::AttrAsBool(options, "normalise"); baton->normaliseLower = sharp::AttrAsUint32(options, "normaliseLower"); diff --git a/src/pipeline.h b/src/pipeline.h index 777a4c543..f50ea2165 100644 --- a/src/pipeline.h +++ b/src/pipeline.h @@ -100,6 +100,8 @@ struct PipelineBaton { int trimOffsetTop; std::vector linearA; std::vector linearB; + int dilateWidth; + int erodeWidth; double gamma; double gammaOut; bool greyscale; @@ -273,6 +275,8 @@ struct PipelineBaton { trimOffsetTop(0), linearA{}, linearB{}, + dilateWidth(0), + erodeWidth(0), gamma(0.0), greyscale(false), normalise(false), diff --git a/test/fixtures/dot-and-lines.png b/test/fixtures/dot-and-lines.png new file mode 100644 index 000000000..5c50d2456 Binary files /dev/null and b/test/fixtures/dot-and-lines.png differ diff --git a/test/fixtures/expected/dilate-1.png b/test/fixtures/expected/dilate-1.png new file mode 100644 index 000000000..947eb4b0c Binary files /dev/null and b/test/fixtures/expected/dilate-1.png differ diff --git a/test/fixtures/expected/erode-1.png b/test/fixtures/expected/erode-1.png new file mode 100644 index 000000000..54ff8035b Binary files /dev/null and b/test/fixtures/expected/erode-1.png differ diff --git a/test/fixtures/index.js b/test/fixtures/index.js index e4b8e266e..0546a3be2 100644 --- a/test/fixtures/index.js +++ b/test/fixtures/index.js @@ -126,6 +126,8 @@ module.exports = { inputJPGBig: getPath('flowers.jpeg'), + inputPngDotAndLines: getPath('dot-and-lines.png'), + inputPngStripesV: getPath('stripesV.png'), inputPngStripesH: getPath('stripesH.png'), diff --git a/test/unit/dilate.js b/test/unit/dilate.js new file mode 100644 index 000000000..94588a285 --- /dev/null +++ b/test/unit/dilate.js @@ -0,0 +1,38 @@ +'use strict'; + +const assert = require('assert'); + +const sharp = require('../../'); +const fixtures = require('../fixtures'); + +describe('Dilate', function () { + it('dilate 1 png', function (done) { + sharp(fixtures.inputPngDotAndLines) + .dilate(1) + .toBuffer(function (err, data, info) { + if (err) throw err; + assert.strictEqual('png', info.format); + assert.strictEqual(100, info.width); + assert.strictEqual(100, info.height); + fixtures.assertSimilar(fixtures.expected('dilate-1.png'), data, done); + }); + }); + + it('dilate 1 png - default width', function (done) { + sharp(fixtures.inputPngDotAndLines) + .dilate() + .toBuffer(function (err, data, info) { + if (err) throw err; + assert.strictEqual('png', info.format); + assert.strictEqual(100, info.width); + assert.strictEqual(100, info.height); + fixtures.assertSimilar(fixtures.expected('dilate-1.png'), data, done); + }); + }); + + it('invalid dilation width', function () { + assert.throws(function () { + sharp(fixtures.inputJpg).dilate(-1); + }); + }); +}); diff --git a/test/unit/erode.js b/test/unit/erode.js new file mode 100644 index 000000000..4d2da81f0 --- /dev/null +++ b/test/unit/erode.js @@ -0,0 +1,38 @@ +'use strict'; + +const assert = require('assert'); + +const sharp = require('../../'); +const fixtures = require('../fixtures'); + +describe('Erode', function () { + it('erode 1 png', function (done) { + sharp(fixtures.inputPngDotAndLines) + .erode(1) + .toBuffer(function (err, data, info) { + if (err) throw err; + assert.strictEqual('png', info.format); + assert.strictEqual(100, info.width); + assert.strictEqual(100, info.height); + fixtures.assertSimilar(fixtures.expected('erode-1.png'), data, done); + }); + }); + + it('erode 1 png - default width', function (done) { + sharp(fixtures.inputPngDotAndLines) + .erode() + .toBuffer(function (err, data, info) { + if (err) throw err; + assert.strictEqual('png', info.format); + assert.strictEqual(100, info.width); + assert.strictEqual(100, info.height); + fixtures.assertSimilar(fixtures.expected('erode-1.png'), data, done); + }); + }); + + it('invalid erosion width', function () { + assert.throws(function () { + sharp(fixtures.inputJpg).erode(-1); + }); + }); +});