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);
+ });
+ });
+});