diff --git a/.gitignore b/.gitignore
index 82f73b3f0..c8617e93d 100644
--- a/.gitignore
+++ b/.gitignore
@@ -17,3 +17,5 @@ package-lock.json
.astro
docs/dist
release-notes.md
+.cache
+compile_commands.json
diff --git a/docs/src/content/docs/api-resize.md b/docs/src/content/docs/api-resize.md
index 6452c3455..364b1b5a7 100644
--- a/docs/src/content/docs/api-resize.md
+++ b/docs/src/content/docs/api-resize.md
@@ -283,6 +283,7 @@ The `info` response Object will contain `trimOffsetLeft` and `trimOffsetTop` pro
| [options] | Object | | |
| [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.margin] | number | 0 | Applies margin in pixels to trim edges leaving extra space around trimmed content. |
| [options.lineArt] | boolean | false | Does the input more closely resemble line art (e.g. vector) rather than being photographic? |
**Example**
@@ -320,4 +321,13 @@ const output = await sharp(input)
threshold: 42,
})
.toBuffer();
+```
+**Example**
+```js
+// Trim image but leave extra space around its content–rectangle of interest.
+const output = await sharp(input)
+ .trim({
+ margin: 10
+ })
+ .toBuffer();
```
\ No newline at end of file
diff --git a/lib/constructor.js b/lib/constructor.js
index 9aac8105c..fdb267738 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 13714c947..09a825f40 100644
--- a/lib/index.d.ts
+++ b/lib/index.d.ts
@@ -1608,6 +1608,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;
+ /** Applies margin in pixels to trim edges leaving extra space around trimmed content. (optional, default 0) */
+ margin?: number | undefined;
}
interface RawOptions {
diff --git a/lib/resize.js b/lib/resize.js
index 544fbba3a..c4d27af5c 100644
--- a/lib/resize.js
+++ b/lib/resize.js
@@ -540,9 +540,18 @@ function extract (options) {
* })
* .toBuffer();
*
+ * @example
+ * // Trim image but leave extra space around its content–rectangle of interest.
+ * 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 {number} [options.margin=0] - Applies margin in pixels to trim edges leaving extra space around trimmed content.
* @param {boolean} [options.lineArt=false] - Does the input more closely resemble line art (e.g. vector) rather than being photographic?
* @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.number(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 0c0d00988..88f09cec5 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 daeba5ab4..94fea1e93 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");
}
@@ -304,6 +304,13 @@ namespace sharp {
} else {
background.resize(image.bands());
}
+ auto applyMargin = [&](int left, int top, int width, int height) {
+ int const marginLeft = std::max(0, left - margin);
+ int const marginTop = std::max(0, top - margin);
+ int const marginWidth = std::min(image.width(), left + width + margin) - marginLeft;
+ int const marginHeight = std::min(image.height(), top + height + margin) - marginTop;
+ return std::make_tuple(marginLeft, marginTop, marginWidth, marginHeight);
+ };
int left, top, width, height;
left = image.find_trim(&top, &width, &height, VImage::option()
->set("background", background)
@@ -324,15 +331,22 @@ namespace sharp {
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;
- return image.extract_area(leftB, topB, widthB, heightB);
+
+ // Combined bounding box + margin
+ auto [ml, mt, mw, mh] = applyMargin(leftB, topB, widthB, heightB);
+ return image.extract_area(ml, mt, mw, mh);
} else {
// Use alpha only
- return image.extract_area(leftA, topA, widthA, heightA);
+ // Bounding box + margin
+ auto [ml, mt, mw, mh] = applyMargin(leftA, topA, widthA, heightA);
+ return image.extract_area(ml, mt, mw, mh);
}
}
}
if (width > 0 && height > 0) {
- return image.extract_area(left, top, width, height);
+ // Bounding box + margin
+ auto [ml, mt, mw, mh] = applyMargin(left, top, width, height);
+ return image.extract_area(ml, mt, mw, mh);
}
return image;
}
diff --git a/src/operations.h b/src/operations.h
index c281c02cd..b9699bb93 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 5f0a3bb0e..c6dff380c 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();
}
@@ -1615,6 +1615,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 ff9465987..8a788245b 100644
--- a/src/pipeline.h
+++ b/src/pipeline.h
@@ -101,6 +101,7 @@ struct PipelineBaton {
bool trimLineArt;
int trimOffsetLeft;
int trimOffsetTop;
+ int trimMargin;
std::vector linearA;
std::vector linearB;
int dilateWidth;
@@ -281,6 +282,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 2448843e2..75bd1bfb9 100644
--- a/test/fixtures/index.js
+++ b/test/fixtures/index.js
@@ -73,6 +73,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'), // https://pixabay.com/photos/cat-bury-cat-animal-sitting-cat-3038243/
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 000000000..6ca1e8faf
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 9364c08c8..8f9b1c6b1 100644
--- a/test/types/sharp.test-d.ts
+++ b/test/types/sharp.test-d.ts
@@ -598,7 +598,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({
@@ -705,20 +705,20 @@ sharp(input)
// https://github.com/lovell/sharp/pull/4048
sharp(input).composite([
{
- input: 'image.gif',
- animated: true,
- limitInputPixels: 536805378,
- density: 144,
+ input: 'image.gif',
+ animated: true,
+ limitInputPixels: 536805378,
+ density: 144,
failOn: "warning",
autoOrient: true
}
])
sharp(input).composite([
{
- input: 'image.png',
+ input: 'image.png',
animated: false,
- limitInputPixels: 178935126,
- density: 72,
+ limitInputPixels: 178935126,
+ density: 72,
failOn: "truncated"
}
])
diff --git a/test/unit/trim.js b/test/unit/trim.js
index 99b14a8d9..ea033a922 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,39 @@ describe('Trim borders', () => {
assert.strictEqual(trimOffsetLeft, 0);
});
});
+
+ describe('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);
+ });
+ });
});