Add trim option to provide a specific background colour (#3332)

Co-authored-by: Mart Jansink <mart@cinemait.nl>
This commit is contained in:
Mart 2022-08-23 13:28:02 +02:00 committed by GitHub
parent 3a44748f49
commit c3a852eecf
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 240 additions and 45 deletions

View File

@ -238,7 +238,7 @@ Returns **Sharp**&#x20;
## trim ## trim
Trim "boring" pixels from all edges that contain values similar to 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.
@ -249,9 +249,51 @@ will contain `trimOffsetLeft` and `trimOffsetTop` properties.
### Parameters ### Parameters
* `threshold` **[number][8]** the allowed difference from the top-left pixel, a number greater than zero. (optional, default `10`) * `trim` **([string][10] | [number][8] | [Object][9])** the specific background colour to trim, the threshold for doing so or an Object with both.
<!----> * `trim.background` **([string][10] | [Object][9])** background colour, parsed by the [color][11] module, defaults to that of the top-left pixel. (optional, default `'top-left pixel'`)
* `trim.threshold` **[number][8]** the allowed difference from the above colour, a positive number. (optional, default `10`)
### Examples
```javascript
// Trim pixels with a colour similar to that of the top-left pixel.
sharp(input)
.trim()
.toFile(output, function(err, info) {
...
});
```
```javascript
// Trim pixels with the exact same colour as that of the top-left pixel.
sharp(input)
.trim(0)
.toFile(output, function(err, info) {
...
});
```
```javascript
// Trim only pixels with a similar colour to red.
sharp(input)
.trim("#FF0000")
.toFile(output, function(err, info) {
...
});
```
```javascript
// Trim all "yellow-ish" pixels, being more lenient with the higher threshold.
sharp(input)
.trim({
background: "yellow",
threshold: 42,
})
.toFile(output, function(err, info) {
...
});
```
* Throws **[Error][13]** Invalid parameters * Throws **[Error][13]** Invalid parameters

File diff suppressed because one or more lines are too long

View File

@ -224,6 +224,7 @@ const Sharp = function (input, options) {
sharpenY3: 20, sharpenY3: 20,
threshold: 0, threshold: 0,
thresholdGrayscale: true, thresholdGrayscale: true,
trimBackground: [],
trimThreshold: 0, trimThreshold: 0,
gamma: 0, gamma: 0,
gammaOut: 0, gammaOut: 0,

View File

@ -450,7 +450,7 @@ function extract (options) {
} }
/** /**
* Trim "boring" pixels from all edges that contain values similar to 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.
* *
@ -459,19 +459,72 @@ function extract (options) {
* The `info` response Object, obtained from callback of `.toFile()` or `.toBuffer()`, * The `info` response Object, obtained from callback of `.toFile()` or `.toBuffer()`,
* will contain `trimOffsetLeft` and `trimOffsetTop` properties. * will contain `trimOffsetLeft` and `trimOffsetTop` properties.
* *
* @param {number} [threshold=10] the allowed difference from the top-left pixel, a number greater than zero. * @example
* // Trim pixels with a colour similar to that of the top-left pixel.
* sharp(input)
* .trim()
* .toFile(output, function(err, info) {
* ...
* });
* @example
* // Trim pixels with the exact same colour as that of the top-left pixel.
* sharp(input)
* .trim(0)
* .toFile(output, function(err, info) {
* ...
* });
* @example
* // Trim only pixels with a similar colour to red.
* sharp(input)
* .trim("#FF0000")
* .toFile(output, function(err, info) {
* ...
* });
* @example
* // Trim all "yellow-ish" pixels, being more lenient with the higher threshold.
* sharp(input)
* .trim({
* background: "yellow",
* threshold: 42,
* })
* .toFile(output, function(err, info) {
* ...
* });
*
* @param {string|number|Object} trim - the specific background colour to trim, the threshold for doing so or an Object with both.
* @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 {number} [trim.threshold=10] - the allowed difference from the above colour, a positive number.
* @returns {Sharp} * @returns {Sharp}
* @throws {Error} Invalid parameters * @throws {Error} Invalid parameters
*/ */
function trim (threshold) { function trim (trim) {
if (!is.defined(threshold)) { if (!is.defined(trim)) {
this.options.trimThreshold = 10; this.options.trimThreshold = 10;
} else if (is.number(threshold) && threshold > 0) { } else if (is.string(trim)) {
this.options.trimThreshold = threshold; this._setBackgroundColourOption('trimBackground', trim);
this.options.trimThreshold = 10;
} else if (is.number(trim)) {
if (trim >= 0) {
this.options.trimThreshold = trim;
} else { } else {
throw is.invalidParameterError('threshold', 'number greater than zero', threshold); throw is.invalidParameterError('threshold', 'positive number', trim);
} }
if (this.options.trimThreshold && isRotationExpected(this.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)) {
if (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)) {
this.options.rotateBeforePreExtract = true; this.options.rotateBeforePreExtract = true;
} }
return this; return this;

View File

@ -84,7 +84,8 @@
"Ompal Singh <ompal.hitm09@gmail.com>", "Ompal Singh <ompal.hitm09@gmail.com>",
"Brodan <christopher.hranj@gmail.com", "Brodan <christopher.hranj@gmail.com",
"Ankur Parihar <ankur.github@gmail.com>", "Ankur Parihar <ankur.github@gmail.com>",
"Brahim Ait elhaj <brahima@gmail.com>" "Brahim Ait elhaj <brahima@gmail.com>",
"Mart Jansink <m.jansink@gmail.com>"
], ],
"scripts": { "scripts": {
"install": "(node install/libvips && node install/dll-copy && prebuild-install) || (node install/can-compile && node-gyp rebuild && node install/dll-copy)", "install": "(node install/libvips && node install/dll-copy && prebuild-install) || (node install/can-compile && node-gyp rebuild && node install/dll-copy)",

View File

@ -262,26 +262,42 @@ namespace sharp {
/* /*
Trim an image Trim an image
*/ */
VImage Trim(VImage image, double const threshold) { VImage Trim(VImage image, std::vector<double> background, double threshold) {
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");
} }
// Top-left pixel provides the background colour
VImage background = image.extract_area(0, 0, 1, 1); // Scale up 8-bit values to match 16-bit input image
if (HasAlpha(background)) { double multiplier = sharp::Is16Bit(image.interpretation()) ? 256.0 : 1.0;
background = background.flatten(); threshold *= multiplier;
std::vector<double> backgroundAlpha(1);
if (background.size() == 0) {
// Top-left pixel provides the default background colour if none is given
background = image.extract_area(0, 0, 1, 1)(0, 0);
multiplier = 1.0;
} }
if (background.size() == 4) {
// Just discard the alpha because flattening the background colour with
// itself (effectively what find_trim() does) gives the same result
backgroundAlpha[0] = background[3] * multiplier;
}
background = {
background[0] * multiplier,
background[1] * multiplier,
background[2] * multiplier
};
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(0, 0)) ->set("background", background)
->set("threshold", threshold)); ->set("threshold", threshold));
if (HasAlpha(image)) { if (HasAlpha(image)) {
// Search alpha channel (A) // Search alpha channel (A)
int leftA, topA, widthA, heightA; int leftA, topA, widthA, heightA;
VImage alpha = image[image.bands() - 1]; VImage alpha = image[image.bands() - 1];
VImage backgroundAlpha = alpha.extract_area(0, 0, 1, 1);
leftA = alpha.find_trim(&topA, &widthA, &heightA, VImage::option() leftA = alpha.find_trim(&topA, &widthA, &heightA, VImage::option()
->set("background", backgroundAlpha(0, 0)) ->set("background", backgroundAlpha)
->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

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

View File

@ -118,7 +118,7 @@ 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::Trim(image, baton->trimThreshold); image = sharp::Trim(image, baton->trimBackground, baton->trimThreshold);
baton->trimOffsetLeft = image.xoffset(); baton->trimOffsetLeft = image.xoffset();
baton->trimOffsetTop = image.yoffset(); baton->trimOffsetTop = image.yoffset();
} }
@ -1451,6 +1451,7 @@ Napi::Value pipeline(const Napi::CallbackInfo& info) {
baton->sharpenY3 = sharp::AttrAsDouble(options, "sharpenY3"); baton->sharpenY3 = sharp::AttrAsDouble(options, "sharpenY3");
baton->threshold = sharp::AttrAsInt32(options, "threshold"); baton->threshold = sharp::AttrAsInt32(options, "threshold");
baton->thresholdGrayscale = sharp::AttrAsBool(options, "thresholdGrayscale"); baton->thresholdGrayscale = sharp::AttrAsBool(options, "thresholdGrayscale");
baton->trimBackground = sharp::AttrAsVectorOfDouble(options, "trimBackground");
baton->trimThreshold = sharp::AttrAsDouble(options, "trimThreshold"); baton->trimThreshold = sharp::AttrAsDouble(options, "trimThreshold");
baton->gamma = sharp::AttrAsDouble(options, "gamma"); baton->gamma = sharp::AttrAsDouble(options, "gamma");
baton->gammaOut = sharp::AttrAsDouble(options, "gammaOut"); baton->gammaOut = sharp::AttrAsDouble(options, "gammaOut");

View File

@ -97,6 +97,7 @@ struct PipelineBaton {
double sharpenY3; double sharpenY3;
int threshold; int threshold;
bool thresholdGrayscale; bool thresholdGrayscale;
std::vector<double> trimBackground;
double trimThreshold; double trimThreshold;
int trimOffsetLeft; int trimOffsetLeft;
int trimOffsetTop; int trimOffsetTop;
@ -248,6 +249,7 @@ struct PipelineBaton {
sharpenY3(20.0), sharpenY3(20.0),
threshold(0), threshold(0),
thresholdGrayscale(true), thresholdGrayscale(true),
trimBackground{},
trimThreshold(0.0), trimThreshold(0.0),
trimOffsetLeft(0), trimOffsetLeft(0),
trimOffsetTop(0), trimOffsetTop(0),

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 812 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 794 B

View File

@ -95,6 +95,9 @@ module.exports = {
inputPngP3: getPath('p3.png'), // https://github.com/lovell/sharp/issues/2862 inputPngP3: getPath('p3.png'), // https://github.com/lovell/sharp/issues/2862
inputPngPalette: getPath('swiss.png'), // https://github.com/randy408/libspng/issues/188 inputPngPalette: getPath('swiss.png'), // https://github.com/randy408/libspng/issues/188
inputPngTrimIncludeAlpha: getPath('trim-mc.png'), // https://github.com/lovell/sharp/issues/2166 inputPngTrimIncludeAlpha: getPath('trim-mc.png'), // https://github.com/lovell/sharp/issues/2166
inputPngTrimSpecificColour: getPath('Flag_of_the_Netherlands.png'), // https://commons.wikimedia.org/wiki/File:Flag_of_the_Netherlands.svg
inputPngTrimSpecificColour16bit: getPath('Flag_of_the_Netherlands-16bit.png'), // convert Flag_of_the_Netherlands.png -depth 16 Flag_of_the_Netherlands-16bit.png
inputPngTrimSpecificColourIncludeAlpha: getPath('Flag_of_the_Netherlands-alpha.png'), // convert Flag_of_the_Netherlands.png -alpha set -background none -channel A -evaluate multiply 0.5 +channel Flag_of_the_Netherlands-alpha.png
inputWebP: getPath('4.webp'), // http://www.gstatic.com/webp/gallery/4.webp inputWebP: getPath('4.webp'), // http://www.gstatic.com/webp/gallery/4.webp
inputWebPWithTransparency: getPath('5_webp_a.webp'), // http://www.gstatic.com/webp/gallery3/5_webp_a.webp inputWebPWithTransparency: getPath('5_webp_a.webp'), // http://www.gstatic.com/webp/gallery3/5_webp_a.webp

View File

@ -7,22 +7,6 @@ const inRange = require('../../lib/is').inRange;
const fixtures = require('../fixtures'); const fixtures = require('../fixtures');
describe('Trim borders', function () { describe('Trim borders', function () {
it('Threshold default', function (done) {
const expected = fixtures.expected('alpha-layer-1-fill-trim-resize.png');
sharp(fixtures.inputPngOverlayLayer1)
.resize(450, 322)
.trim()
.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);
});
});
it('Skip shrink-on-load', function (done) { it('Skip shrink-on-load', function (done) {
const expected = fixtures.expected('alpha-layer-2-trim-resize.jpg'); const expected = fixtures.expected('alpha-layer-2-trim-resize.jpg');
sharp(fixtures.inputJpgOverlayLayer2) sharp(fixtures.inputJpgOverlayLayer2)
@ -41,7 +25,7 @@ describe('Trim borders', function () {
}); });
}); });
it('single colour PNG where alpha channel provides the image', () => it('Single colour PNG where alpha channel provides the image', () =>
sharp(fixtures.inputPngImageInAlpha) sharp(fixtures.inputPngImageInAlpha)
.trim() .trim()
.toBuffer({ resolveWithObject: true }) .toBuffer({ resolveWithObject: true })
@ -94,7 +78,7 @@ describe('Trim borders', function () {
.catch(done); .catch(done);
}); });
it('should rotate before trim', () => it('Should rotate before trim', () =>
sharp({ sharp({
create: { create: {
width: 20, width: 20,
@ -159,13 +143,105 @@ describe('Trim borders', function () {
assert.strictEqual(trimOffsetLeft, 0); assert.strictEqual(trimOffsetLeft, 0);
}); });
describe('Invalid thresholds', function () { describe('Valid parameters', function () {
[-1, 'fail', {}].forEach(function (threshold) { const expected = fixtures.expected('alpha-layer-1-fill-trim-resize.png');
it(JSON.stringify(threshold), function () { Object.entries({
'Background and threshold default': undefined,
'Background string': '#00000000',
'Background option': {
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 () {
Object.entries({
'Invalid background string': 'fail',
'Invalid background option': {
background: 'fail'
},
'Negative threshold number': -1,
'Negative threshold option': {
threshold: -1
},
Boolean: false
}).forEach(function ([description, parameter]) {
it(description, function () {
assert.throws(function () { assert.throws(function () {
sharp().trim(threshold); sharp().trim(parameter);
}); });
}); });
}); });
}); });
describe('Specific background colour', function () {
it('Doesn\'t trim at all', async () => {
const { info } = await sharp(fixtures.inputPngTrimSpecificColour)
.trim('yellow')
.toBuffer({ resolveWithObject: true });
const { width, height, trimOffsetTop, trimOffsetLeft } = info;
assert.strictEqual(width, 900);
assert.strictEqual(height, 600);
assert.strictEqual(trimOffsetTop, 0);
assert.strictEqual(trimOffsetLeft, 0);
});
it('Only trims the bottom', async () => {
const { info } = await sharp(fixtures.inputPngTrimSpecificColour)
.trim('#21468B')
.toBuffer({ resolveWithObject: true });
const { width, height, trimOffsetTop, trimOffsetLeft } = info;
assert.strictEqual(width, 900);
assert.strictEqual(height, 401);
assert.strictEqual(trimOffsetTop, 0);
assert.strictEqual(trimOffsetLeft, 0);
});
it('Only trims the bottom, in 16-bit', async () => {
const { info } = await sharp(fixtures.inputPngTrimSpecificColour16bit)
.trim('#21468B')
.toBuffer({ resolveWithObject: true });
const { width, height, trimOffsetTop, trimOffsetLeft } = info;
assert.strictEqual(width, 900);
assert.strictEqual(height, 401);
assert.strictEqual(trimOffsetTop, 0);
assert.strictEqual(trimOffsetLeft, 0);
});
it('Only trims the bottom, including alpha', async () => {
const { info } = await sharp(fixtures.inputPngTrimSpecificColourIncludeAlpha)
.trim('#21468B80')
.toBuffer({ resolveWithObject: true });
const { width, height, trimOffsetTop, trimOffsetLeft } = info;
assert.strictEqual(width, 900);
assert.strictEqual(height, 401);
assert.strictEqual(trimOffsetTop, 0);
assert.strictEqual(trimOffsetLeft, 0);
});
});
}); });