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