mirror of
https://github.com/lovell/sharp.git
synced 2026-02-04 05:36:18 +01:00
Add margin option to trim operation #4480
This commit is contained in:
committed by
Lovell Fuller
parent
d161e45e06
commit
a5e726002c
@@ -326,3 +326,6 @@ GitHub: https://github.com/tpatel
|
|||||||
|
|
||||||
Name: Maël Nison
|
Name: Maël Nison
|
||||||
GitHub: https://github.com/arcanis
|
GitHub: https://github.com/arcanis
|
||||||
|
|
||||||
|
Name: Dmytro Tiapukhin
|
||||||
|
GitHub: https://github.com/eddienubes
|
||||||
|
|||||||
@@ -284,6 +284,7 @@ The `info` response Object will contain `trimOffsetLeft` and `trimOffsetTop` pro
|
|||||||
| [options.background] | <code>string</code> \| <code>Object</code> | <code>"'top-left pixel'"</code> | Background colour, parsed by the [color](https://www.npmjs.org/package/color) module, defaults to that of the top-left pixel. |
|
| [options.background] | <code>string</code> \| <code>Object</code> | <code>"'top-left pixel'"</code> | Background colour, parsed by the [color](https://www.npmjs.org/package/color) module, defaults to that of the top-left pixel. |
|
||||||
| [options.threshold] | <code>number</code> | <code>10</code> | Allowed difference from the above colour, a positive number. |
|
| [options.threshold] | <code>number</code> | <code>10</code> | Allowed difference from the above colour, a positive number. |
|
||||||
| [options.lineArt] | <code>boolean</code> | <code>false</code> | Does the input more closely resemble line art (e.g. vector) rather than being photographic? |
|
| [options.lineArt] | <code>boolean</code> | <code>false</code> | Does the input more closely resemble line art (e.g. vector) rather than being photographic? |
|
||||||
|
| [options.margin] | <code>number</code> | <code>0</code> | Leave a margin around trimmed content, value is in pixels. |
|
||||||
|
|
||||||
**Example**
|
**Example**
|
||||||
```js
|
```js
|
||||||
@@ -321,3 +322,12 @@ const output = await sharp(input)
|
|||||||
})
|
})
|
||||||
.toBuffer();
|
.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();
|
||||||
|
```
|
||||||
@@ -34,4 +34,8 @@ slug: changelog/v0.35.0
|
|||||||
* TypeScript: Ensure `FormatEnum` keys match reality.
|
* TypeScript: Ensure `FormatEnum` keys match reality.
|
||||||
[#4475](https://github.com/lovell/sharp/issues/4475)
|
[#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.
|
* Add WebP `exact` option for control over transparent pixel colour values.
|
||||||
|
|||||||
@@ -278,6 +278,7 @@ const Sharp = function (input, options) {
|
|||||||
trimBackground: [],
|
trimBackground: [],
|
||||||
trimThreshold: -1,
|
trimThreshold: -1,
|
||||||
trimLineArt: false,
|
trimLineArt: false,
|
||||||
|
trimMargin: 0,
|
||||||
dilateWidth: 0,
|
dilateWidth: 0,
|
||||||
erodeWidth: 0,
|
erodeWidth: 0,
|
||||||
gamma: 0,
|
gamma: 0,
|
||||||
|
|||||||
2
lib/index.d.ts
vendored
2
lib/index.d.ts
vendored
@@ -1606,6 +1606,8 @@ declare namespace sharp {
|
|||||||
threshold?: number | undefined;
|
threshold?: number | undefined;
|
||||||
/** Does the input more closely resemble line art (e.g. vector) rather than being photographic? (optional, default false) */
|
/** Does the input more closely resemble line art (e.g. vector) rather than being photographic? (optional, default false) */
|
||||||
lineArt?: boolean | undefined;
|
lineArt?: boolean | undefined;
|
||||||
|
/** Leave a margin around trimmed content, value is in pixels. (optional, default 0) */
|
||||||
|
margin?: number | undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface RawOptions {
|
interface RawOptions {
|
||||||
|
|||||||
@@ -540,10 +540,19 @@ function extract (options) {
|
|||||||
* })
|
* })
|
||||||
* .toBuffer();
|
* .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 {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 {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.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 {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}
|
* @returns {Sharp}
|
||||||
* @throws {Error} Invalid parameters
|
* @throws {Error} Invalid parameters
|
||||||
*/
|
*/
|
||||||
@@ -564,6 +573,13 @@ function trim (options) {
|
|||||||
if (is.defined(options.lineArt)) {
|
if (is.defined(options.lineArt)) {
|
||||||
this._setBooleanOption('trimLineArt', 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 {
|
} else {
|
||||||
throw is.invalidParameterError('trim', 'object', options);
|
throw is.invalidParameterError('trim', 'object', options);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -89,7 +89,8 @@
|
|||||||
"Lachlan Newman <lachnewman007@gmail.com>",
|
"Lachlan Newman <lachnewman007@gmail.com>",
|
||||||
"Dennis Beatty <dennis@dcbeatty.com>",
|
"Dennis Beatty <dennis@dcbeatty.com>",
|
||||||
"Ingvar Stepanyan <me@rreverser.com>",
|
"Ingvar Stepanyan <me@rreverser.com>",
|
||||||
"Don Denton <don@happycollision.com>"
|
"Don Denton <don@happycollision.com>",
|
||||||
|
"Dmytro Tiapukhin <cool.gegeg@gmail.com>"
|
||||||
],
|
],
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "node install/build.js",
|
"build": "node install/build.js",
|
||||||
|
|||||||
@@ -285,7 +285,7 @@ namespace sharp {
|
|||||||
/*
|
/*
|
||||||
Trim an image
|
Trim an image
|
||||||
*/
|
*/
|
||||||
VImage Trim(VImage image, std::vector<double> background, double threshold, bool const lineArt) {
|
VImage Trim(VImage image, std::vector<double> background, double threshold, bool const lineArt, int const margin) {
|
||||||
if (image.width() < 3 && image.height() < 3) {
|
if (image.width() < 3 && image.height() < 3) {
|
||||||
throw VError("Image to trim must be at least 3x3 pixels");
|
throw VError("Image to trim must be at least 3x3 pixels");
|
||||||
}
|
}
|
||||||
@@ -320,18 +320,36 @@ namespace sharp {
|
|||||||
if (widthA > 0 && heightA > 0) {
|
if (widthA > 0 && heightA > 0) {
|
||||||
if (width > 0 && height > 0) {
|
if (width > 0 && height > 0) {
|
||||||
// Combined bounding box (B)
|
// Combined bounding box (B)
|
||||||
int const leftB = std::min(left, leftA);
|
int leftB = std::min(left, leftA);
|
||||||
int const topB = std::min(top, topA);
|
int topB = std::min(top, topA);
|
||||||
int const widthB = std::max(left + width, leftA + widthA) - leftB;
|
int widthB = std::max(left + width, leftA + widthA) - leftB;
|
||||||
int const heightB = std::max(top + height, topA + heightA) - topB;
|
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);
|
return image.extract_area(leftB, topB, widthB, heightB);
|
||||||
} else {
|
} else {
|
||||||
// Use alpha only
|
// 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);
|
return image.extract_area(leftA, topA, widthA, heightA);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (width > 0 && height > 0) {
|
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.extract_area(left, top, width, height);
|
||||||
}
|
}
|
||||||
return image;
|
return image;
|
||||||
|
|||||||
@@ -82,7 +82,7 @@ namespace sharp {
|
|||||||
/*
|
/*
|
||||||
Trim an image
|
Trim an image
|
||||||
*/
|
*/
|
||||||
VImage Trim(VImage image, std::vector<double> background, double threshold, bool const lineArt);
|
VImage Trim(VImage image, std::vector<double> background, double threshold, bool const lineArt, int const margin);
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Linear adjustment (a * in + b)
|
* Linear adjustment (a * in + b)
|
||||||
|
|||||||
@@ -153,7 +153,7 @@ class PipelineWorker : public Napi::AsyncWorker {
|
|||||||
if (baton->trimThreshold >= 0.0) {
|
if (baton->trimThreshold >= 0.0) {
|
||||||
MultiPageUnsupported(nPages, "Trim");
|
MultiPageUnsupported(nPages, "Trim");
|
||||||
image = sharp::StaySequential(image);
|
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->trimOffsetLeft = image.xoffset();
|
||||||
baton->trimOffsetTop = image.yoffset();
|
baton->trimOffsetTop = image.yoffset();
|
||||||
}
|
}
|
||||||
@@ -1637,6 +1637,7 @@ Napi::Value pipeline(const Napi::CallbackInfo& info) {
|
|||||||
baton->trimBackground = sharp::AttrAsVectorOfDouble(options, "trimBackground");
|
baton->trimBackground = sharp::AttrAsVectorOfDouble(options, "trimBackground");
|
||||||
baton->trimThreshold = sharp::AttrAsDouble(options, "trimThreshold");
|
baton->trimThreshold = sharp::AttrAsDouble(options, "trimThreshold");
|
||||||
baton->trimLineArt = sharp::AttrAsBool(options, "trimLineArt");
|
baton->trimLineArt = sharp::AttrAsBool(options, "trimLineArt");
|
||||||
|
baton->trimMargin = sharp::AttrAsUint32(options, "trimMargin");
|
||||||
baton->gamma = sharp::AttrAsDouble(options, "gamma");
|
baton->gamma = sharp::AttrAsDouble(options, "gamma");
|
||||||
baton->gammaOut = sharp::AttrAsDouble(options, "gammaOut");
|
baton->gammaOut = sharp::AttrAsDouble(options, "gammaOut");
|
||||||
baton->linearA = sharp::AttrAsVectorOfDouble(options, "linearA");
|
baton->linearA = sharp::AttrAsVectorOfDouble(options, "linearA");
|
||||||
|
|||||||
@@ -102,6 +102,7 @@ struct PipelineBaton {
|
|||||||
bool trimLineArt;
|
bool trimLineArt;
|
||||||
int trimOffsetLeft;
|
int trimOffsetLeft;
|
||||||
int trimOffsetTop;
|
int trimOffsetTop;
|
||||||
|
int trimMargin;
|
||||||
std::vector<double> linearA;
|
std::vector<double> linearA;
|
||||||
std::vector<double> linearB;
|
std::vector<double> linearB;
|
||||||
int dilateWidth;
|
int dilateWidth;
|
||||||
@@ -286,6 +287,7 @@ struct PipelineBaton {
|
|||||||
trimLineArt(false),
|
trimLineArt(false),
|
||||||
trimOffsetLeft(0),
|
trimOffsetLeft(0),
|
||||||
trimOffsetTop(0),
|
trimOffsetTop(0),
|
||||||
|
trimMargin(0),
|
||||||
linearA{},
|
linearA{},
|
||||||
linearB{},
|
linearB{},
|
||||||
dilateWidth(0),
|
dilateWidth(0),
|
||||||
|
|||||||
1
test/fixtures/index.js
vendored
1
test/fixtures/index.js
vendored
@@ -74,6 +74,7 @@ module.exports = {
|
|||||||
|
|
||||||
inputPng: getPath('50020484-00001.png'), // http://c.searspartsdirect.com/lis_png/PLDM/50020484-00001.png
|
inputPng: getPath('50020484-00001.png'), // http://c.searspartsdirect.com/lis_png/PLDM/50020484-00001.png
|
||||||
inputPngGradients: getPath('gradients-rgb8.png'),
|
inputPngGradients: getPath('gradients-rgb8.png'),
|
||||||
|
inputPngWithSlightGradientBorder: getPath('slight-gradient-border.png'),
|
||||||
inputPngWithTransparency: getPath('blackbug.png'), // public domain
|
inputPngWithTransparency: getPath('blackbug.png'), // public domain
|
||||||
inputPngCompleteTransparency: getPath('full-transparent.png'),
|
inputPngCompleteTransparency: getPath('full-transparent.png'),
|
||||||
inputPngWithGreyAlpha: getPath('grey-8bit-alpha.png'),
|
inputPngWithGreyAlpha: getPath('grey-8bit-alpha.png'),
|
||||||
|
|||||||
BIN
test/fixtures/slight-gradient-border.png
vendored
Normal file
BIN
test/fixtures/slight-gradient-border.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 34 KiB |
@@ -599,7 +599,7 @@ const vertexSplitQuadraticBasisSpline: string = sharp.interpolators.vertexSplitQ
|
|||||||
// Triming
|
// Triming
|
||||||
sharp(input).trim({ background: '#000' }).toBuffer();
|
sharp(input).trim({ background: '#000' }).toBuffer();
|
||||||
sharp(input).trim({ threshold: 10, lineArt: true }).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
|
// Text input
|
||||||
sharp({
|
sharp({
|
||||||
|
|||||||
@@ -222,6 +222,9 @@ describe('Trim borders', () => {
|
|||||||
},
|
},
|
||||||
'Invalid lineArt': {
|
'Invalid lineArt': {
|
||||||
lineArt: 'fail'
|
lineArt: 'fail'
|
||||||
|
},
|
||||||
|
'Invalid margin': {
|
||||||
|
margin: -1
|
||||||
}
|
}
|
||||||
}).forEach(([description, parameter]) => {
|
}).forEach(([description, parameter]) => {
|
||||||
it(description, () => {
|
it(description, () => {
|
||||||
@@ -289,4 +292,42 @@ describe('Trim borders', () => {
|
|||||||
assert.strictEqual(trimOffsetLeft, 0);
|
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);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user