diff --git a/docs/public/humans.txt b/docs/public/humans.txt
index 4c9eb88d..f8bd272f 100644
--- a/docs/public/humans.txt
+++ b/docs/public/humans.txt
@@ -314,3 +314,6 @@ GitHub: https://github.com/happycollision
Name: Florent Zabera
GitHub: https://github.com/florentzabera
+
+Name: Quentin Pinçon
+GitHub: https://github.com/qpincon
diff --git a/docs/src/content/docs/api-operation.md b/docs/src/content/docs/api-operation.md
index 720918c5..dea7c79e 100644
--- a/docs/src/content/docs/api-operation.md
+++ b/docs/src/content/docs/api-operation.md
@@ -284,6 +284,52 @@ const gaussianBlurred = await sharp(input)
```
+## dilate
+> dilate([width]) ⇒ Sharp
+
+Expand foreground objects using the dilate morphological operator.
+
+
+**Throws**:
+
+- Error
Invalid parameters
+
+
+| Param | Type | Default | Description |
+| --- | --- | --- | --- |
+| [width] | Number
| 1
| dilation width in pixels. |
+
+**Example**
+```js
+const output = await sharp(input)
+ .dilate()
+ .toBuffer();
+```
+
+
+## erode
+> erode([width]) ⇒ Sharp
+
+Shrink foreground objects using the erode morphological operator.
+
+
+**Throws**:
+
+- Error
Invalid parameters
+
+
+| Param | Type | Default | Description |
+| --- | --- | --- | --- |
+| [width] | Number
| 1
| erosion width in pixels. |
+
+**Example**
+```js
+const output = await sharp(input)
+ .erode()
+ .toBuffer();
+```
+
+
## flatten
> flatten([options]) ⇒ Sharp
diff --git a/docs/src/content/docs/changelog.md b/docs/src/content/docs/changelog.md
index 67d222d6..e08b5d8b 100644
--- a/docs/src/content/docs/changelog.md
+++ b/docs/src/content/docs/changelog.md
@@ -45,6 +45,10 @@ Requires libvips v8.16.1
[#4207](https://github.com/lovell/sharp/pull/4207)
[@calebmer](https://github.com/calebmer)
+* Expose erode and dilate operations.
+ [#4243](https://github.com/lovell/sharp/pull/4243)
+ [@qpincon](https://github.com/qpincon)
+
* Add support for RGBE images. Requires libvips compiled with radiance support.
[#4316](https://github.com/lovell/sharp/pull/4316)
[@florentzabera](https://github.com/florentzabera)
diff --git a/lib/constructor.js b/lib/constructor.js
index 32a80ede..3b88fbd9 100644
--- a/lib/constructor.js
+++ b/lib/constructor.js
@@ -263,6 +263,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/index.d.ts b/lib/index.d.ts
index 78837a94..a1c5f082 100644
--- a/lib/index.d.ts
+++ b/lib/index.d.ts
@@ -504,6 +504,22 @@ declare namespace sharp {
*/
blur(sigma?: number | boolean | BlurOptions): Sharp;
+ /**
+ * Expand foreground objects using the dilate morphological operator.
+ * @param {Number} [width=1] dilation width in pixels.
+ * @throws {Error} Invalid parameters
+ * @returns A sharp instance that can be used to chain operations
+ */
+ dilate(width?: number): Sharp;
+
+ /**
+ * Shrink foreground objects using the erode morphological operator.
+ * @param {Number} [width=1] erosion width in pixels.
+ * @throws {Error} Invalid parameters
+ * @returns A sharp instance that can be used to chain operations
+ */
+ erode(width?: number): Sharp;
+
/**
* Merge alpha transparency channel, if any, with background.
* @param flatten true to enable and false to disable (defaults to true)
diff --git a/lib/operation.js b/lib/operation.js
index b65cebbf..d6cbbdf8 100644
--- a/lib/operation.js
+++ b/lib/operation.js
@@ -443,6 +443,52 @@ function blur (options) {
return this;
}
+/**
+ * Expand foreground objects using the dilate morphological operator.
+ *
+ * @example
+ * const output = await sharp(input)
+ * .dilate()
+ * .toBuffer();
+ *
+ * @param {Number} [width=1] dilation width in pixels.
+ * @returns {Sharp}
+ * @throws {Error} Invalid parameters
+ */
+function dilate (width) {
+ if (!is.defined(width)) {
+ this.options.dilateWidth = 1;
+ } else if (is.integer(width) && width > 0) {
+ this.options.dilateWidth = width;
+ } else {
+ throw is.invalidParameterError('dilate', 'positive integer', dilate);
+ }
+ return this;
+}
+
+/**
+ * Shrink foreground objects using the erode morphological operator.
+ *
+ * @example
+ * const output = await sharp(input)
+ * .erode()
+ * .toBuffer();
+ *
+ * @param {Number} [width=1] erosion width in pixels.
+ * @returns {Sharp}
+ * @throws {Error} Invalid parameters
+ */
+function erode (width) {
+ if (!is.defined(width)) {
+ this.options.erodeWidth = 1;
+ } else if (is.integer(width) && width > 0) {
+ 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.
*
@@ -958,6 +1004,8 @@ module.exports = function (Sharp) {
flop,
affine,
sharpen,
+ erode,
+ dilate,
median,
blur,
flatten,
diff --git a/src/operations.cc b/src/operations.cc
index 9856bb2a..ba3a0513 100644
--- a/src/operations.cc
+++ b/src/operations.cc
@@ -472,4 +472,26 @@ namespace sharp {
}
}
+ /*
+ * Dilate an image
+ */
+ VImage Dilate(VImage image, int const width) {
+ int const 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) {
+ int const 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 b2881d65..22ff46fc 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 24ae7e02..21529acf 100644
--- a/src/pipeline.cc
+++ b/src/pipeline.cc
@@ -609,6 +609,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);
@@ -1621,6 +1631,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 90d6f85c..4bb053fd 100644
--- a/src/pipeline.h
+++ b/src/pipeline.h
@@ -101,6 +101,8 @@ struct PipelineBaton {
int trimOffsetTop;
std::vector linearA;
std::vector linearB;
+ int dilateWidth;
+ int erodeWidth;
double gamma;
double gammaOut;
bool greyscale;
@@ -274,6 +276,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 00000000..5c50d245
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 00000000..947eb4b0
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 00000000..54ff8035
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 ba79504d..f5b686ad 100644
--- a/test/fixtures/index.js
+++ b/test/fixtures/index.js
@@ -128,6 +128,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/types/sharp.test-d.ts b/test/types/sharp.test-d.ts
index 93b421ba..d968e07c 100644
--- a/test/types/sharp.test-d.ts
+++ b/test/types/sharp.test-d.ts
@@ -740,3 +740,8 @@ sharp([input, input], {
valign: 'bottom'
}
});
+
+sharp().erode();
+sharp().erode(1);
+sharp().dilate();
+sharp().dilate(1);
diff --git a/test/unit/dilate.js b/test/unit/dilate.js
new file mode 100644
index 00000000..94588a28
--- /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 00000000..4d2da81f
--- /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);
+ });
+ });
+});