Options for trim op must be an Object, add lineArt #2363

This commit is contained in:
Lovell Fuller 2023-11-04 14:09:50 +00:00
parent 2e7c60675b
commit 0bd1715f36
12 changed files with 118 additions and 125 deletions

View File

@ -251,7 +251,7 @@ sharp(input)
## trim ## trim
> trim(trim) ⇒ <code>Sharp</code> > trim([options]) ⇒ <code>Sharp</code>
Trim pixels from all edges that contain values similar to the given background colour, which defaults to that of the top-left pixel. Trim pixels from all edges that contain values similar to the given background colour, which defaults to that of the top-left pixel.
@ -259,8 +259,7 @@ Images with an alpha channel will use the combined bounding box of alpha and non
If the result of this operation would trim an image to nothing then no change is made. If the result of this operation would trim an image to nothing then no change is made.
The `info` response Object, obtained from callback of `.toFile()` or `.toBuffer()`, The `info` response Object will contain `trimOffsetLeft` and `trimOffsetTop` properties.
will contain `trimOffsetLeft` and `trimOffsetTop` properties.
**Throws**: **Throws**:
@ -270,46 +269,44 @@ will contain `trimOffsetLeft` and `trimOffsetTop` properties.
| Param | Type | Default | Description | | Param | Type | Default | Description |
| --- | --- | --- | --- | | --- | --- | --- | --- |
| trim | <code>string</code> \| <code>number</code> \| <code>Object</code> | | the specific background colour to trim, the threshold for doing so or an Object with both. | | [options] | <code>Object</code> | | |
| [trim.background] | <code>string</code> \| <code>Object</code> | <code>&quot;&#x27;top-left pixel&#x27;&quot;</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>&quot;&#x27;top-left pixel&#x27;&quot;</code> | Background colour, parsed by the [color](https://www.npmjs.org/package/color) module, defaults to that of the top-left pixel. |
| [trim.threshold] | <code>number</code> | <code>10</code> | the 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? |
**Example** **Example**
```js ```js
// Trim pixels with a colour similar to that of the top-left pixel. // Trim pixels with a colour similar to that of the top-left pixel.
sharp(input) await sharp(input)
.trim() .trim()
.toFile(output, function(err, info) { .toFile(output);
...
});
``` ```
**Example** **Example**
```js ```js
// Trim pixels with the exact same colour as that of the top-left pixel. // Trim pixels with the exact same colour as that of the top-left pixel.
sharp(input) await sharp(input)
.trim(0) .trim({
.toFile(output, function(err, info) { threshold: 0
... })
}); .toFile(output);
``` ```
**Example** **Example**
```js ```js
// Trim only pixels with a similar colour to red. // Assume input is line art and trim only pixels with a similar colour to red.
sharp(input) const output = await sharp(input)
.trim("#FF0000") .trim({
.toFile(output, function(err, info) { background: "#FF0000",
... lineArt: true
}); })
.toBuffer();
``` ```
**Example** **Example**
```js ```js
// Trim all "yellow-ish" pixels, being more lenient with the higher threshold. // Trim all "yellow-ish" pixels, being more lenient with the higher threshold.
sharp(input) const output = await sharp(input)
.trim({ .trim({
background: "yellow", background: "yellow",
threshold: 42, threshold: 42,
}) })
.toFile(output, function(err, info) { .toBuffer();
...
});
``` ```

View File

@ -14,6 +14,9 @@ Requires libvips v8.15.0
* Remove `sharp.vendor`. * Remove `sharp.vendor`.
* Options for `trim` operation must be an Object, add new `lineArt` option.
[#2363](https://github.com/lovell/sharp/issues/2363)
* Ensure all `Error` objects contain a `stack` property. * Ensure all `Error` objects contain a `stack` property.
[#3653](https://github.com/lovell/sharp/issues/3653) [#3653](https://github.com/lovell/sharp/issues/3653)

File diff suppressed because one or more lines are too long

View File

@ -230,7 +230,8 @@ const Sharp = function (input, options) {
threshold: 0, threshold: 0,
thresholdGrayscale: true, thresholdGrayscale: true,
trimBackground: [], trimBackground: [],
trimThreshold: 0, trimThreshold: -1,
trimLineArt: false,
gamma: 0, gamma: 0,
gammaOut: 0, gammaOut: 0,
greyscale: false, greyscale: false,

10
lib/index.d.ts vendored
View File

@ -847,11 +847,11 @@ declare namespace sharp {
* Trim pixels from all edges that contain values similar to the given background colour, which defaults to that of the top-left pixel. * Trim pixels from all edges that contain values similar to the given background colour, which defaults to that of the top-left pixel.
* Images with an alpha channel will use the combined bounding box of alpha and non-alpha channels. * Images with an alpha channel will use the combined bounding box of alpha and non-alpha channels.
* The info response Object will contain trimOffsetLeft and trimOffsetTop properties. * The info response Object will contain trimOffsetLeft and trimOffsetTop properties.
* @param trim The specific background colour to trim, the threshold for doing so or an Object with both. * @param options trim options
* @throws {Error} Invalid parameters * @throws {Error} Invalid parameters
* @returns A sharp instance that can be used to chain operations * @returns A sharp instance that can be used to chain operations
*/ */
trim(trim?: string | number | TrimOptions): Sharp; trim(options?: TrimOptions): Sharp;
//#endregion //#endregion
} }
@ -1342,10 +1342,12 @@ declare namespace sharp {
} }
interface TrimOptions { interface TrimOptions {
/** background colour, parsed by the color module, defaults to that of the top-left pixel. (optional) */ /** Background colour, parsed by the color module, defaults to that of the top-left pixel. (optional) */
background?: Color | undefined; background?: Color | undefined;
/** the allowed difference from the above colour, a positive number. (optional, default `10`) */ /** Allowed difference from the above colour, a positive number. (optional, default 10) */
threshold?: number | undefined; threshold?: number | undefined;
/** Does the input more closely resemble line art (e.g. vector) rather than being photographic? (optional, default false) */
lineArt?: boolean | undefined;
} }
interface RawOptions { interface RawOptions {

View File

@ -494,70 +494,67 @@ function extract (options) {
* *
* If the result of this operation would trim an image to nothing then no change is made. * If the result of this operation would trim an image to nothing then no change is made.
* *
* The `info` response Object, obtained from callback of `.toFile()` or `.toBuffer()`, * The `info` response Object will contain `trimOffsetLeft` and `trimOffsetTop` properties.
* will contain `trimOffsetLeft` and `trimOffsetTop` properties.
* *
* @example * @example
* // Trim pixels with a colour similar to that of the top-left pixel. * // Trim pixels with a colour similar to that of the top-left pixel.
* sharp(input) * await sharp(input)
* .trim() * .trim()
* .toFile(output, function(err, info) { * .toFile(output);
* ... *
* });
* @example * @example
* // Trim pixels with the exact same colour as that of the top-left pixel. * // Trim pixels with the exact same colour as that of the top-left pixel.
* sharp(input) * await sharp(input)
* .trim(0) * .trim({
* .toFile(output, function(err, info) { * threshold: 0
* ... * })
* }); * .toFile(output);
*
* @example * @example
* // Trim only pixels with a similar colour to red. * // Assume input is line art and trim only pixels with a similar colour to red.
* sharp(input) * const output = await sharp(input)
* .trim("#FF0000") * .trim({
* .toFile(output, function(err, info) { * background: "#FF0000",
* ... * lineArt: true
* }); * })
* .toBuffer();
*
* @example * @example
* // Trim all "yellow-ish" pixels, being more lenient with the higher threshold. * // Trim all "yellow-ish" pixels, being more lenient with the higher threshold.
* sharp(input) * const output = await sharp(input)
* .trim({ * .trim({
* background: "yellow", * background: "yellow",
* threshold: 42, * threshold: 42,
* }) * })
* .toFile(output, function(err, info) { * .toBuffer();
* ...
* });
* *
* @param {string|number|Object} trim - the specific background colour to trim, the threshold for doing so or an Object with both. * @param {Object} [options]
* @param {string|Object} [trim.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} [trim.threshold=10] - the 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?
* @returns {Sharp} * @returns {Sharp}
* @throws {Error} Invalid parameters * @throws {Error} Invalid parameters
*/ */
function trim (trim) { function trim (options) {
if (!is.defined(trim)) { this.options.trimThreshold = 10;
this.options.trimThreshold = 10; if (is.defined(options)) {
} else if (is.string(trim)) { if (is.object(options)) {
this._setBackgroundColourOption('trimBackground', trim); if (is.defined(options.background)) {
this.options.trimThreshold = 10; this._setBackgroundColourOption('trimBackground', options.background);
} else if (is.number(trim)) { }
if (trim >= 0) { if (is.defined(options.threshold)) {
this.options.trimThreshold = trim; if (is.number(options.threshold) && options.threshold >= 0) {
this.options.trimThreshold = options.threshold;
} else {
throw is.invalidParameterError('threshold', 'positive number', options.threshold);
}
}
if (is.defined(options.lineArt)) {
this._setBooleanOption('trimLineArt', options.lineArt);
}
} else { } else {
throw is.invalidParameterError('threshold', 'positive number', trim); throw is.invalidParameterError('trim', 'object', options);
} }
} else if (is.object(trim)) {
this._setBackgroundColourOption('trimBackground', trim.background);
if (!is.defined(trim.threshold)) {
this.options.trimThreshold = 10;
} else if (is.number(trim.threshold) && trim.threshold >= 0) {
this.options.trimThreshold = trim.threshold;
} else {
throw is.invalidParameterError('threshold', 'positive number', trim);
}
} else {
throw is.invalidParameterError('trim', 'string, number or object', trim);
} }
if (isRotationExpected(this.options)) { if (isRotationExpected(this.options)) {
this.options.rotateBeforePreExtract = true; this.options.rotateBeforePreExtract = true;

View File

@ -265,7 +265,7 @@ namespace sharp {
/* /*
Trim an image Trim an image
*/ */
VImage Trim(VImage image, std::vector<double> background, double threshold) { VImage Trim(VImage image, std::vector<double> background, double threshold, bool const lineArt) {
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");
} }
@ -287,6 +287,7 @@ namespace sharp {
int left, top, width, height; int left, top, width, height;
left = image.find_trim(&top, &width, &height, VImage::option() left = image.find_trim(&top, &width, &height, VImage::option()
->set("background", background) ->set("background", background)
->set("line_art", lineArt)
->set("threshold", threshold)); ->set("threshold", threshold));
if (HasAlpha(image)) { if (HasAlpha(image)) {
// Search alpha channel (A) // Search alpha channel (A)
@ -294,6 +295,7 @@ namespace sharp {
VImage alpha = image[image.bands() - 1]; VImage alpha = image[image.bands() - 1];
leftA = alpha.find_trim(&topA, &widthA, &heightA, VImage::option() leftA = alpha.find_trim(&topA, &widthA, &heightA, VImage::option()
->set("background", backgroundAlpha) ->set("background", backgroundAlpha)
->set("line_art", lineArt)
->set("threshold", threshold)); ->set("threshold", threshold));
if (widthA > 0 && heightA > 0) { if (widthA > 0 && heightA > 0) {
if (width > 0 && height > 0) { if (width > 0 && height > 0) {

View File

@ -79,7 +79,7 @@ namespace sharp {
/* /*
Trim an image Trim an image
*/ */
VImage Trim(VImage image, std::vector<double> background, double const threshold); VImage Trim(VImage image, std::vector<double> background, double threshold, bool const lineArt);
/* /*
* Linear adjustment (a * in + b) * Linear adjustment (a * in + b)

View File

@ -126,10 +126,10 @@ class PipelineWorker : public Napi::AsyncWorker {
} }
// Trim // Trim
if (baton->trimThreshold > 0.0) { if (baton->trimThreshold >= 0.0) {
MultiPageUnsupported(nPages, "Trim"); MultiPageUnsupported(nPages, "Trim");
image = sharp::StaySequential(image, access); image = sharp::StaySequential(image, access);
image = sharp::Trim(image, baton->trimBackground, baton->trimThreshold); image = sharp::Trim(image, baton->trimBackground, baton->trimThreshold, baton->trimLineArt);
baton->trimOffsetLeft = image.xoffset(); baton->trimOffsetLeft = image.xoffset();
baton->trimOffsetTop = image.yoffset(); baton->trimOffsetTop = image.yoffset();
} }
@ -182,7 +182,7 @@ class PipelineWorker : public Napi::AsyncWorker {
// - trimming or pre-resize extract isn't required; // - trimming or pre-resize extract isn't required;
// - input colourspace is not specified; // - input colourspace is not specified;
bool const shouldPreShrink = (targetResizeWidth > 0 || targetResizeHeight > 0) && bool const shouldPreShrink = (targetResizeWidth > 0 || targetResizeHeight > 0) &&
baton->gamma == 0 && baton->topOffsetPre == -1 && baton->trimThreshold == 0.0 && baton->gamma == 0 && baton->topOffsetPre == -1 && baton->trimThreshold < 0.0 &&
baton->colourspaceInput == VIPS_INTERPRETATION_LAST && !shouldRotateBefore; baton->colourspaceInput == VIPS_INTERPRETATION_LAST && !shouldRotateBefore;
if (shouldPreShrink) { if (shouldPreShrink) {
@ -1248,11 +1248,10 @@ class PipelineWorker : public Napi::AsyncWorker {
info.Set("attentionX", static_cast<int32_t>(baton->attentionX)); info.Set("attentionX", static_cast<int32_t>(baton->attentionX));
info.Set("attentionY", static_cast<int32_t>(baton->attentionY)); info.Set("attentionY", static_cast<int32_t>(baton->attentionY));
} }
if (baton->trimThreshold > 0.0) { if (baton->trimThreshold >= 0.0) {
info.Set("trimOffsetLeft", static_cast<int32_t>(baton->trimOffsetLeft)); info.Set("trimOffsetLeft", static_cast<int32_t>(baton->trimOffsetLeft));
info.Set("trimOffsetTop", static_cast<int32_t>(baton->trimOffsetTop)); info.Set("trimOffsetTop", static_cast<int32_t>(baton->trimOffsetTop));
} }
if (baton->input->textAutofitDpi) { if (baton->input->textAutofitDpi) {
info.Set("textAutofitDpi", static_cast<uint32_t>(baton->input->textAutofitDpi)); info.Set("textAutofitDpi", static_cast<uint32_t>(baton->input->textAutofitDpi));
} }
@ -1519,6 +1518,7 @@ Napi::Value pipeline(const Napi::CallbackInfo& info) {
baton->thresholdGrayscale = sharp::AttrAsBool(options, "thresholdGrayscale"); baton->thresholdGrayscale = sharp::AttrAsBool(options, "thresholdGrayscale");
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->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");

View File

@ -92,6 +92,7 @@ struct PipelineBaton {
bool thresholdGrayscale; bool thresholdGrayscale;
std::vector<double> trimBackground; std::vector<double> trimBackground;
double trimThreshold; double trimThreshold;
bool trimLineArt;
int trimOffsetLeft; int trimOffsetLeft;
int trimOffsetTop; int trimOffsetTop;
std::vector<double> linearA; std::vector<double> linearA;
@ -260,7 +261,8 @@ struct PipelineBaton {
threshold(0), threshold(0),
thresholdGrayscale(true), thresholdGrayscale(true),
trimBackground{}, trimBackground{},
trimThreshold(0.0), trimThreshold(-1.0),
trimLineArt(false),
trimOffsetLeft(0), trimOffsetLeft(0),
trimOffsetTop(0), trimOffsetTop(0),
linearA{}, linearA{},

View File

@ -572,8 +572,8 @@ const nohalo: string = sharp.interpolators.nohalo;
const vertexSplitQuadraticBasisSpline: string = sharp.interpolators.vertexSplitQuadraticBasisSpline; const vertexSplitQuadraticBasisSpline: string = sharp.interpolators.vertexSplitQuadraticBasisSpline;
// Triming // Triming
sharp(input).trim('#000').toBuffer(); sharp(input).trim({ background: '#000' }).toBuffer();
sharp(input).trim(10).toBuffer(); sharp(input).trim({ threshold: 10, lineArt: true }).toBuffer();
sharp(input).trim({ background: '#bf1942', threshold: 30 }).toBuffer(); sharp(input).trim({ background: '#bf1942', threshold: 30 }).toBuffer();
// Text input // Text input

View File

@ -46,7 +46,9 @@ describe('Trim borders', function () {
it('16-bit PNG with alpha channel', function (done) { it('16-bit PNG with alpha channel', function (done) {
sharp(fixtures.inputPngWithTransparency16bit) sharp(fixtures.inputPngWithTransparency16bit)
.resize(32, 32) .resize(32, 32)
.trim(20) .trim({
threshold: 20
})
.toBuffer(function (err, data, info) { .toBuffer(function (err, data, info) {
if (err) throw err; if (err) throw err;
assert.strictEqual(true, data.length > 0); assert.strictEqual(true, data.length > 0);
@ -96,7 +98,9 @@ describe('Trim borders', function () {
.then(rotated30 => .then(rotated30 =>
sharp(rotated30) sharp(rotated30)
.rotate(-30) .rotate(-30)
.trim(128) .trim({
threshold: 128
})
.toBuffer({ resolveWithObject: true }) .toBuffer({ resolveWithObject: true })
.then(({ info }) => { .then(({ info }) => {
assert.strictEqual(20, info.width); assert.strictEqual(20, info.width);
@ -198,49 +202,26 @@ describe('Trim borders', function () {
assert.strictEqual(trimOffsetLeft, 0); assert.strictEqual(trimOffsetLeft, 0);
}); });
describe('Valid parameters', function () { it('Works with line-art', async () => {
const expected = fixtures.expected('alpha-layer-1-fill-trim-resize.png'); const { info } = await sharp(fixtures.inputJpgOverlayLayer2)
Object.entries({ .trim({ lineArt: true })
'Background and threshold default': undefined, .toBuffer({ resolveWithObject: true });
'Background string': '#00000000',
'Background option': { assert.strictEqual(info.trimOffsetTop, -552);
background: '#00000000'
},
'Threshold number': 10,
'Threshold option': {
threshold: 10
}
}).forEach(function ([description, parameter]) {
it(description, function (done) {
sharp(fixtures.inputPngOverlayLayer1)
.resize(450, 322)
.trim(parameter)
.toBuffer(function (err, data, info) {
if (err) throw err;
assert.strictEqual('png', info.format);
assert.strictEqual(450, info.width);
assert.strictEqual(322, info.height);
assert.strictEqual(-204, info.trimOffsetLeft);
assert.strictEqual(0, info.trimOffsetTop);
fixtures.assertSimilar(expected, data, done);
});
});
});
}); });
describe('Invalid parameters', function () { describe('Invalid parameters', function () {
Object.entries({ Object.entries({
'Invalid background string': 'fail', 'Invalid string': 'fail',
'Invalid background option': { 'Invalid background option': {
background: 'fail' background: 'fail'
}, },
'Negative threshold number': -1,
'Negative threshold option': { 'Negative threshold option': {
threshold: -1 threshold: -1
}, },
'Invalid lineArt': {
Boolean: false lineArt: 'fail'
}
}).forEach(function ([description, parameter]) { }).forEach(function ([description, parameter]) {
it(description, function () { it(description, function () {
assert.throws(function () { assert.throws(function () {
@ -253,7 +234,9 @@ describe('Trim borders', function () {
describe('Specific background colour', function () { describe('Specific background colour', function () {
it('Doesn\'t trim at all', async () => { it('Doesn\'t trim at all', async () => {
const { info } = await sharp(fixtures.inputPngTrimSpecificColour) const { info } = await sharp(fixtures.inputPngTrimSpecificColour)
.trim('yellow') .trim({
background: 'yellow'
})
.toBuffer({ resolveWithObject: true }); .toBuffer({ resolveWithObject: true });
const { width, height, trimOffsetTop, trimOffsetLeft } = info; const { width, height, trimOffsetTop, trimOffsetLeft } = info;
@ -265,7 +248,9 @@ describe('Trim borders', function () {
it('Only trims the bottom', async () => { it('Only trims the bottom', async () => {
const { info } = await sharp(fixtures.inputPngTrimSpecificColour) const { info } = await sharp(fixtures.inputPngTrimSpecificColour)
.trim('#21468B') .trim({
background: '#21468B'
})
.toBuffer({ resolveWithObject: true }); .toBuffer({ resolveWithObject: true });
const { width, height, trimOffsetTop, trimOffsetLeft } = info; const { width, height, trimOffsetTop, trimOffsetLeft } = info;
@ -277,7 +262,9 @@ describe('Trim borders', function () {
it('Only trims the bottom, in 16-bit', async () => { it('Only trims the bottom, in 16-bit', async () => {
const { info } = await sharp(fixtures.inputPngTrimSpecificColour16bit) const { info } = await sharp(fixtures.inputPngTrimSpecificColour16bit)
.trim('#21468B') .trim({
background: '#21468B'
})
.toBuffer({ resolveWithObject: true }); .toBuffer({ resolveWithObject: true });
const { width, height, trimOffsetTop, trimOffsetLeft } = info; const { width, height, trimOffsetTop, trimOffsetLeft } = info;
@ -289,7 +276,9 @@ describe('Trim borders', function () {
it('Only trims the bottom, including alpha', async () => { it('Only trims the bottom, including alpha', async () => {
const { info } = await sharp(fixtures.inputPngTrimSpecificColourIncludeAlpha) const { info } = await sharp(fixtures.inputPngTrimSpecificColourIncludeAlpha)
.trim('#21468B80') .trim({
background: '#21468B80'
})
.toBuffer({ resolveWithObject: true }); .toBuffer({ resolveWithObject: true });
const { width, height, trimOffsetTop, trimOffsetLeft } = info; const { width, height, trimOffsetTop, trimOffsetLeft } = info;