diff --git a/docs/public/humans.txt b/docs/public/humans.txt
index 7119152a..00efa0d7 100644
--- a/docs/public/humans.txt
+++ b/docs/public/humans.txt
@@ -326,3 +326,6 @@ GitHub: https://github.com/tpatel
Name: Maƫl Nison
GitHub: https://github.com/arcanis
+
+Name: Dmytro Tiapukhin
+GitHub: https://github.com/eddienubes
diff --git a/docs/src/content/docs/api-resize.md b/docs/src/content/docs/api-resize.md
index 6452c345..254e4560 100644
--- a/docs/src/content/docs/api-resize.md
+++ b/docs/src/content/docs/api-resize.md
@@ -284,6 +284,7 @@ The `info` response Object will contain `trimOffsetLeft` and `trimOffsetTop` pro
| [options.background] | string \| Object | "'top-left pixel'" | Background colour, parsed by the [color](https://www.npmjs.org/package/color) module, defaults to that of the top-left pixel. |
| [options.threshold] | number | 10 | Allowed difference from the above colour, a positive number. |
| [options.lineArt] | boolean | false | Does the input more closely resemble line art (e.g. vector) rather than being photographic? |
+| [options.margin] | number | 0 | Leave a margin around trimmed content, value is in pixels. |
**Example**
```js
@@ -320,4 +321,13 @@ const output = await sharp(input)
threshold: 42,
})
.toBuffer();
+```
+**Example**
+```js
+// Trim image leaving (up to) a 10 pixel margin around the trimmed content.
+const output = await sharp(input)
+ .trim({
+ margin: 10
+ })
+ .toBuffer();
```
\ No newline at end of file
diff --git a/docs/src/content/docs/changelog/v0.35.0.md b/docs/src/content/docs/changelog/v0.35.0.md
index be1ae2c2..6e92ba0d 100644
--- a/docs/src/content/docs/changelog/v0.35.0.md
+++ b/docs/src/content/docs/changelog/v0.35.0.md
@@ -34,4 +34,8 @@ slug: changelog/v0.35.0
* TypeScript: Ensure `FormatEnum` keys match reality.
[#4475](https://github.com/lovell/sharp/issues/4475)
+* Add `margin` option to `trim` operation.
+ [#4480](https://github.com/lovell/sharp/issues/4480)
+ [@eddienubes](https://github.com/eddienubes)
+
* Add WebP `exact` option for control over transparent pixel colour values.
diff --git a/lib/constructor.js b/lib/constructor.js
index 99267ae6..1a2e55be 100644
--- a/lib/constructor.js
+++ b/lib/constructor.js
@@ -278,6 +278,7 @@ const Sharp = function (input, options) {
trimBackground: [],
trimThreshold: -1,
trimLineArt: false,
+ trimMargin: 0,
dilateWidth: 0,
erodeWidth: 0,
gamma: 0,
diff --git a/lib/index.d.ts b/lib/index.d.ts
index b3c2f818..fd36ecf9 100644
--- a/lib/index.d.ts
+++ b/lib/index.d.ts
@@ -1606,6 +1606,8 @@ declare namespace sharp {
threshold?: number | undefined;
/** Does the input more closely resemble line art (e.g. vector) rather than being photographic? (optional, default false) */
lineArt?: boolean | undefined;
+ /** Leave a margin around trimmed content, value is in pixels. (optional, default 0) */
+ margin?: number | undefined;
}
interface RawOptions {
diff --git a/lib/resize.js b/lib/resize.js
index 544fbba3..8084e55e 100644
--- a/lib/resize.js
+++ b/lib/resize.js
@@ -540,10 +540,19 @@ function extract (options) {
* })
* .toBuffer();
*
+ * @example
+ * // Trim image leaving (up to) a 10 pixel margin around the trimmed content.
+ * const output = await sharp(input)
+ * .trim({
+ * margin: 10
+ * })
+ * .toBuffer();
+ *
* @param {Object} [options]
* @param {string|Object} [options.background='top-left pixel'] - Background colour, parsed by the [color](https://www.npmjs.org/package/color) module, defaults to that of the top-left pixel.
* @param {number} [options.threshold=10] - Allowed difference from the above colour, a positive number.
* @param {boolean} [options.lineArt=false] - Does the input more closely resemble line art (e.g. vector) rather than being photographic?
+ * @param {number} [options.margin=0] - Leave a margin around trimmed content, value is in pixels.
* @returns {Sharp}
* @throws {Error} Invalid parameters
*/
@@ -564,6 +573,13 @@ function trim (options) {
if (is.defined(options.lineArt)) {
this._setBooleanOption('trimLineArt', options.lineArt);
}
+ if (is.defined(options.margin)) {
+ if (is.integer(options.margin) && options.margin >= 0) {
+ this.options.trimMargin = options.margin;
+ } else {
+ throw is.invalidParameterError('margin', 'positive integer', options.margin);
+ }
+ }
} else {
throw is.invalidParameterError('trim', 'object', options);
}
diff --git a/package.json b/package.json
index 73db59c9..6802b150 100644
--- a/package.json
+++ b/package.json
@@ -89,7 +89,8 @@
"Lachlan Newman ",
"Dennis Beatty ",
"Ingvar Stepanyan ",
- "Don Denton "
+ "Don Denton ",
+ "Dmytro Tiapukhin "
],
"scripts": {
"build": "node install/build.js",
diff --git a/src/operations.cc b/src/operations.cc
index daeba5ab..50b4bed2 100644
--- a/src/operations.cc
+++ b/src/operations.cc
@@ -285,7 +285,7 @@ namespace sharp {
/*
Trim an image
*/
- VImage Trim(VImage image, std::vector background, double threshold, bool const lineArt) {
+ VImage Trim(VImage image, std::vector background, double threshold, bool const lineArt, int const margin) {
if (image.width() < 3 && image.height() < 3) {
throw VError("Image to trim must be at least 3x3 pixels");
}
@@ -320,18 +320,36 @@ namespace sharp {
if (widthA > 0 && heightA > 0) {
if (width > 0 && height > 0) {
// Combined bounding box (B)
- int const leftB = std::min(left, leftA);
- int const topB = std::min(top, topA);
- int const widthB = std::max(left + width, leftA + widthA) - leftB;
- int const heightB = std::max(top + height, topA + heightA) - topB;
+ int leftB = std::min(left, leftA);
+ int topB = std::min(top, topA);
+ int widthB = std::max(left + width, leftA + widthA) - leftB;
+ int heightB = std::max(top + height, topA + heightA) - topB;
+ if (margin > 0) {
+ leftB = std::max(0, leftB - margin);
+ topB = std::max(0, topB - margin);
+ widthB = std::min(image.width() - leftB, widthB + 2 * margin);
+ heightB = std::min(image.height() - topB, heightB + 2 * margin);
+ }
return image.extract_area(leftB, topB, widthB, heightB);
} else {
// Use alpha only
+ if (margin > 0) {
+ leftA = std::max(0, leftA - margin);
+ topA = std::max(0, topA - margin);
+ widthA = std::min(image.width() - leftA, widthA + 2 * margin);
+ heightA = std::min(image.height() - topA, heightA + 2 * margin);
+ }
return image.extract_area(leftA, topA, widthA, heightA);
}
}
}
if (width > 0 && height > 0) {
+ if (margin > 0) {
+ left = std::max(0, left - margin);
+ top = std::max(0, top - margin);
+ width = std::min(image.width() - left, width + 2 * margin);
+ height = std::min(image.height() - top, height + 2 * margin);
+ }
return image.extract_area(left, top, width, height);
}
return image;
diff --git a/src/operations.h b/src/operations.h
index c281c02c..b9699bb9 100644
--- a/src/operations.h
+++ b/src/operations.h
@@ -82,7 +82,7 @@ namespace sharp {
/*
Trim an image
*/
- VImage Trim(VImage image, std::vector background, double threshold, bool const lineArt);
+ VImage Trim(VImage image, std::vector background, double threshold, bool const lineArt, int const margin);
/*
* Linear adjustment (a * in + b)
diff --git a/src/pipeline.cc b/src/pipeline.cc
index 3ede4a35..c45279a2 100644
--- a/src/pipeline.cc
+++ b/src/pipeline.cc
@@ -153,7 +153,7 @@ class PipelineWorker : public Napi::AsyncWorker {
if (baton->trimThreshold >= 0.0) {
MultiPageUnsupported(nPages, "Trim");
image = sharp::StaySequential(image);
- image = sharp::Trim(image, baton->trimBackground, baton->trimThreshold, baton->trimLineArt);
+ image = sharp::Trim(image, baton->trimBackground, baton->trimThreshold, baton->trimLineArt, baton->trimMargin);
baton->trimOffsetLeft = image.xoffset();
baton->trimOffsetTop = image.yoffset();
}
@@ -1637,6 +1637,7 @@ Napi::Value pipeline(const Napi::CallbackInfo& info) {
baton->trimBackground = sharp::AttrAsVectorOfDouble(options, "trimBackground");
baton->trimThreshold = sharp::AttrAsDouble(options, "trimThreshold");
baton->trimLineArt = sharp::AttrAsBool(options, "trimLineArt");
+ baton->trimMargin = sharp::AttrAsUint32(options, "trimMargin");
baton->gamma = sharp::AttrAsDouble(options, "gamma");
baton->gammaOut = sharp::AttrAsDouble(options, "gammaOut");
baton->linearA = sharp::AttrAsVectorOfDouble(options, "linearA");
diff --git a/src/pipeline.h b/src/pipeline.h
index 718faaf3..a007f7a8 100644
--- a/src/pipeline.h
+++ b/src/pipeline.h
@@ -102,6 +102,7 @@ struct PipelineBaton {
bool trimLineArt;
int trimOffsetLeft;
int trimOffsetTop;
+ int trimMargin;
std::vector linearA;
std::vector linearB;
int dilateWidth;
@@ -286,6 +287,7 @@ struct PipelineBaton {
trimLineArt(false),
trimOffsetLeft(0),
trimOffsetTop(0),
+ trimMargin(0),
linearA{},
linearB{},
dilateWidth(0),
diff --git a/test/fixtures/index.js b/test/fixtures/index.js
index c58d42fd..3cd588dd 100644
--- a/test/fixtures/index.js
+++ b/test/fixtures/index.js
@@ -74,6 +74,7 @@ module.exports = {
inputPng: getPath('50020484-00001.png'), // http://c.searspartsdirect.com/lis_png/PLDM/50020484-00001.png
inputPngGradients: getPath('gradients-rgb8.png'),
+ inputPngWithSlightGradientBorder: getPath('slight-gradient-border.png'),
inputPngWithTransparency: getPath('blackbug.png'), // public domain
inputPngCompleteTransparency: getPath('full-transparent.png'),
inputPngWithGreyAlpha: getPath('grey-8bit-alpha.png'),
diff --git a/test/fixtures/slight-gradient-border.png b/test/fixtures/slight-gradient-border.png
new file mode 100644
index 00000000..e43caea7
Binary files /dev/null and b/test/fixtures/slight-gradient-border.png differ
diff --git a/test/types/sharp.test-d.ts b/test/types/sharp.test-d.ts
index fdf76af1..b415df00 100644
--- a/test/types/sharp.test-d.ts
+++ b/test/types/sharp.test-d.ts
@@ -599,7 +599,7 @@ const vertexSplitQuadraticBasisSpline: string = sharp.interpolators.vertexSplitQ
// Triming
sharp(input).trim({ background: '#000' }).toBuffer();
sharp(input).trim({ threshold: 10, lineArt: true }).toBuffer();
-sharp(input).trim({ background: '#bf1942', threshold: 30 }).toBuffer();
+sharp(input).trim({ background: '#bf1942', threshold: 30, margin: 20 }).toBuffer();
// Text input
sharp({
diff --git a/test/unit/trim.js b/test/unit/trim.js
index 99b14a8d..f17582f9 100644
--- a/test/unit/trim.js
+++ b/test/unit/trim.js
@@ -222,6 +222,9 @@ describe('Trim borders', () => {
},
'Invalid lineArt': {
lineArt: 'fail'
+ },
+ 'Invalid margin': {
+ margin: -1
}
}).forEach(([description, parameter]) => {
it(description, () => {
@@ -289,4 +292,42 @@ describe('Trim borders', () => {
assert.strictEqual(trimOffsetLeft, 0);
});
});
+
+ describe('Adds margin around content', () => {
+ it('Should trim complex gradients', async () => {
+ const { info } = await sharp(fixtures.inputPngGradients)
+ .trim({ threshold: 50, margin: 100 })
+ .toBuffer({ resolveWithObject: true });
+
+ const { width, height, trimOffsetTop, trimOffsetLeft } = info;
+ assert.strictEqual(width, 1000);
+ assert.strictEqual(height, 443);
+ assert.strictEqual(trimOffsetTop, -557);
+ assert.strictEqual(trimOffsetLeft, 0);
+ });
+
+ it('Should trim simple gradients', async () => {
+ const { info } = await sharp(fixtures.inputPngWithSlightGradientBorder)
+ .trim({ threshold: 70, margin: 50 })
+ .toBuffer({ resolveWithObject: true });
+
+ const { width, height, trimOffsetTop, trimOffsetLeft } = info;
+ assert.strictEqual(width, 900);
+ assert.strictEqual(height, 900);
+ assert.strictEqual(trimOffsetTop, -50);
+ assert.strictEqual(trimOffsetLeft, -50);
+ });
+
+ it('Should not overflow image bounding box', async () => {
+ const { info } = await sharp(fixtures.inputPngWithSlightGradientBorder)
+ .trim({ threshold: 70, margin: 9999999 })
+ .toBuffer({ resolveWithObject: true });
+
+ const { width, height, trimOffsetTop, trimOffsetLeft } = info;
+ assert.strictEqual(width, 1000);
+ assert.strictEqual(height, 1000);
+ assert.strictEqual(trimOffsetTop, 0);
+ assert.strictEqual(trimOffsetLeft, 0);
+ });
+ });
});