From a5e726002c30413500698aa562c85b9366e823eb Mon Sep 17 00:00:00 2001 From: Dmytro Tiapukhin Date: Fri, 2 Jan 2026 09:32:31 +0000 Subject: [PATCH] Add margin option to trim operation #4480 --- docs/public/humans.txt | 3 ++ docs/src/content/docs/api-resize.md | 10 +++++ docs/src/content/docs/changelog/v0.35.0.md | 4 ++ lib/constructor.js | 1 + lib/index.d.ts | 2 + lib/resize.js | 16 ++++++++ package.json | 3 +- src/operations.cc | 28 +++++++++++--- src/operations.h | 2 +- src/pipeline.cc | 3 +- src/pipeline.h | 2 + test/fixtures/index.js | 1 + test/fixtures/slight-gradient-border.png | Bin 0 -> 34946 bytes test/types/sharp.test-d.ts | 2 +- test/unit/trim.js | 41 +++++++++++++++++++++ 15 files changed, 109 insertions(+), 9 deletions(-) create mode 100644 test/fixtures/slight-gradient-border.png 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 0000000000000000000000000000000000000000..e43caea7c78fde57238077a66b9281f7cf8436d3 GIT binary patch literal 34946 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 { }, '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); + }); + }); });