Add withoutReduction option to resize operation (#3006)

This commit is contained in:
Chris Banks 2021-12-12 14:10:56 -05:00 committed by GitHub
parent 3b492ea423
commit 446e4e3c3a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 173 additions and 1 deletions

View File

@ -49,6 +49,7 @@ Possible interpolation kernels are:
* `options.background` **([String][10] | [Object][9])** background colour when using a `fit` of `contain`, parsed by the [color][11] module, defaults to black without transparency. (optional, default `{r:0,g:0,b:0,alpha:1}`) * `options.background` **([String][10] | [Object][9])** background colour when using a `fit` of `contain`, parsed by the [color][11] module, defaults to black without transparency. (optional, default `{r:0,g:0,b:0,alpha:1}`)
* `options.kernel` **[String][10]** the kernel to use for image reduction. (optional, default `'lanczos3'`) * `options.kernel` **[String][10]** the kernel to use for image reduction. (optional, default `'lanczos3'`)
* `options.withoutEnlargement` **[Boolean][12]** do not enlarge if the width *or* height are already less than the specified dimensions, equivalent to GraphicsMagick's `>` geometry option. (optional, default `false`) * `options.withoutEnlargement` **[Boolean][12]** do not enlarge if the width *or* height are already less than the specified dimensions, equivalent to GraphicsMagick's `>` geometry option. (optional, default `false`)
* `options.withoutReduction` **[Boolean][12]** do not reduce if the width *or* height are already greater than the specified dimensions, equivalent to GraphicsMagick's `<` geometry option. (optional, default `false`)
* `options.fastShrinkOnLoad` **[Boolean][12]** take greater advantage of the JPEG and WebP shrink-on-load feature, which can lead to a slight moiré pattern on some images. (optional, default `true`) * `options.fastShrinkOnLoad` **[Boolean][12]** take greater advantage of the JPEG and WebP shrink-on-load feature, which can lead to a slight moiré pattern on some images. (optional, default `true`)
### Examples ### Examples
@ -117,6 +118,21 @@ sharp(input)
}); });
``` ```
```javascript
sharp(input)
.resize(200, 200, {
fit: sharp.fit.outside,
withoutReduction: true
})
.toFormat('jpeg')
.toBuffer()
.then(function(outputBuffer) {
// outputBuffer contains JPEG image data
// of at least 200 pixels wide and 200 pixels high while maintaining aspect ratio
// and no smaller than the input image
});
```
```javascript ```javascript
const scaleByHalf = await sharp(input) const scaleByHalf = await sharp(input)
.metadata() .metadata()

View File

@ -166,6 +166,7 @@ const Sharp = function (input, options) {
extendRight: 0, extendRight: 0,
extendBackground: [0, 0, 0, 255], extendBackground: [0, 0, 0, 255],
withoutEnlargement: false, withoutEnlargement: false,
withoutReduction: false,
affineMatrix: [], affineMatrix: [],
affineBackground: [0, 0, 0, 255], affineBackground: [0, 0, 0, 255],
affineIdx: 0, affineIdx: 0,

View File

@ -183,6 +183,20 @@ function isRotationExpected (options) {
* }); * });
* *
* @example * @example
* sharp(input)
* .resize(200, 200, {
* fit: sharp.fit.outside,
* withoutReduction: true
* })
* .toFormat('jpeg')
* .toBuffer()
* .then(function(outputBuffer) {
* // outputBuffer contains JPEG image data
* // of at least 200 pixels wide and 200 pixels high while maintaining aspect ratio
* // and no smaller than the input image
* });
*
* @example
* const scaleByHalf = await sharp(input) * const scaleByHalf = await sharp(input)
* .metadata() * .metadata()
* .then(({ width }) => sharp(input) * .then(({ width }) => sharp(input)
@ -200,6 +214,7 @@ function isRotationExpected (options) {
* @param {String|Object} [options.background={r: 0, g: 0, b: 0, alpha: 1}] - background colour when using a `fit` of `contain`, parsed by the [color](https://www.npmjs.org/package/color) module, defaults to black without transparency. * @param {String|Object} [options.background={r: 0, g: 0, b: 0, alpha: 1}] - background colour when using a `fit` of `contain`, parsed by the [color](https://www.npmjs.org/package/color) module, defaults to black without transparency.
* @param {String} [options.kernel='lanczos3'] - the kernel to use for image reduction. * @param {String} [options.kernel='lanczos3'] - the kernel to use for image reduction.
* @param {Boolean} [options.withoutEnlargement=false] - do not enlarge if the width *or* height are already less than the specified dimensions, equivalent to GraphicsMagick's `>` geometry option. * @param {Boolean} [options.withoutEnlargement=false] - do not enlarge if the width *or* height are already less than the specified dimensions, equivalent to GraphicsMagick's `>` geometry option.
* @param {Boolean} [options.withoutReduction=false] - do not reduce if the width *or* height are already greater than the specified dimensions, equivalent to GraphicsMagick's `<` geometry option.
* @param {Boolean} [options.fastShrinkOnLoad=true] - take greater advantage of the JPEG and WebP shrink-on-load feature, which can lead to a slight moiré pattern on some images. * @param {Boolean} [options.fastShrinkOnLoad=true] - take greater advantage of the JPEG and WebP shrink-on-load feature, which can lead to a slight moiré pattern on some images.
* @returns {Sharp} * @returns {Sharp}
* @throws {Error} Invalid parameters * @throws {Error} Invalid parameters
@ -276,6 +291,10 @@ function resize (width, height, options) {
if (is.defined(options.withoutEnlargement)) { if (is.defined(options.withoutEnlargement)) {
this._setBooleanOption('withoutEnlargement', options.withoutEnlargement); this._setBooleanOption('withoutEnlargement', options.withoutEnlargement);
} }
// Without reduction
if (is.defined(options.withoutReduction)) {
this._setBooleanOption('withoutReduction', options.withoutReduction);
}
// Shrink on load // Shrink on load
if (is.defined(options.fastShrinkOnLoad)) { if (is.defined(options.fastShrinkOnLoad)) {
this._setBooleanOption('fastShrinkOnLoad', options.fastShrinkOnLoad); this._setBooleanOption('fastShrinkOnLoad', options.fastShrinkOnLoad);

View File

@ -79,7 +79,8 @@
"Michael Nutt <michael@nutt.im>", "Michael Nutt <michael@nutt.im>",
"Brad Parham <baparham@gmail.com>", "Brad Parham <baparham@gmail.com>",
"Taneli Vatanen <taneli.vatanen@gmail.com>", "Taneli Vatanen <taneli.vatanen@gmail.com>",
"Joris Dugué <zaruike10@gmail.com>" "Joris Dugué <zaruike10@gmail.com>",
"Chris Banks <christopher.bradley.banks@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

@ -130,6 +130,17 @@ class PipelineWorker : public Napi::AsyncWorker {
pageHeight = inputHeight; pageHeight = inputHeight;
} }
// If withoutReduction is specified,
// Override target width and height if less than respective value from input file
if (baton->withoutReduction) {
if (baton->width < inputWidth) {
baton->width = inputWidth;
}
if (baton->height < inputHeight) {
baton->height = inputHeight;
}
}
// Scaling calculations // Scaling calculations
double hshrink; double hshrink;
double vshrink; double vshrink;
@ -1356,6 +1367,7 @@ Napi::Value pipeline(const Napi::CallbackInfo& info) {
} }
// Resize options // Resize options
baton->withoutEnlargement = sharp::AttrAsBool(options, "withoutEnlargement"); baton->withoutEnlargement = sharp::AttrAsBool(options, "withoutEnlargement");
baton->withoutReduction = sharp::AttrAsBool(options, "withoutReduction");
baton->position = sharp::AttrAsInt32(options, "position"); baton->position = sharp::AttrAsInt32(options, "position");
baton->resizeBackground = sharp::AttrAsVectorOfDouble(options, "resizeBackground"); baton->resizeBackground = sharp::AttrAsVectorOfDouble(options, "resizeBackground");
baton->kernel = sharp::AttrAsStr(options, "kernel"); baton->kernel = sharp::AttrAsStr(options, "kernel");

View File

@ -119,6 +119,7 @@ struct PipelineBaton {
int extendRight; int extendRight;
std::vector<double> extendBackground; std::vector<double> extendBackground;
bool withoutEnlargement; bool withoutEnlargement;
bool withoutReduction;
std::vector<double> affineMatrix; std::vector<double> affineMatrix;
std::vector<double> affineBackground; std::vector<double> affineBackground;
double affineIdx; double affineIdx;
@ -259,6 +260,7 @@ struct PipelineBaton {
extendRight(0), extendRight(0),
extendBackground{ 0.0, 0.0, 0.0, 255.0 }, extendBackground{ 0.0, 0.0, 0.0, 255.0 },
withoutEnlargement(false), withoutEnlargement(false),
withoutReduction(false),
affineMatrix{ 1.0, 0.0, 0.0, 1.0 }, affineMatrix{ 1.0, 0.0, 0.0, 1.0 },
affineBackground{ 0.0, 0.0, 0.0, 255.0 }, affineBackground{ 0.0, 0.0, 0.0, 255.0 },
affineIdx(0), affineIdx(0),

View File

@ -357,6 +357,127 @@ describe('Resize dimensions', function () {
}); });
}); });
it('Do enlarge when input width is less than output width', function (done) {
sharp(fixtures.inputJpg)
.resize({
width: 2800,
withoutReduction: true
})
.toBuffer(function (err, data, info) {
if (err) throw err;
assert.strictEqual(true, data.length > 0);
assert.strictEqual('jpeg', info.format);
assert.strictEqual(2800, info.width);
assert.strictEqual(2225, info.height);
done();
});
});
it('Do enlarge when input height is less than output height', function (done) {
sharp(fixtures.inputJpg)
.resize({
height: 2300,
withoutReduction: true
})
.toBuffer(function (err, data, info) {
if (err) throw err;
assert.strictEqual(true, data.length > 0);
assert.strictEqual('jpeg', info.format);
assert.strictEqual(2725, info.width);
assert.strictEqual(2300, info.height);
done();
});
});
it('Do not crop when fit = cover and withoutReduction = true and width >= outputWidth, and height < outputHeight', function (done) {
sharp(fixtures.inputJpg)
.resize({
width: 3000,
height: 1000,
withoutReduction: true
})
.toBuffer(function (err, data, info) {
if (err) throw err;
assert.strictEqual(true, data.length > 0);
assert.strictEqual('jpeg', info.format);
assert.strictEqual(3000, info.width);
assert.strictEqual(2225, info.height);
done();
});
});
it('Do not crop when fit = cover and withoutReduction = true and width < outputWidth, and height >= outputHeight', function (done) {
sharp(fixtures.inputJpg)
.resize({
width: 1500,
height: 2226,
withoutReduction: true
})
.toBuffer(function (err, data, info) {
if (err) throw err;
assert.strictEqual(true, data.length > 0);
assert.strictEqual('jpeg', info.format);
assert.strictEqual(2725, info.width);
assert.strictEqual(2226, info.height);
done();
});
});
it('Do enlarge when input width is less than output width', function (done) {
sharp(fixtures.inputJpg)
.resize({
width: 2800,
withoutReduction: false
})
.toBuffer(function (err, data, info) {
if (err) throw err;
assert.strictEqual(true, data.length > 0);
assert.strictEqual('jpeg', info.format);
assert.strictEqual(2800, info.width);
assert.strictEqual(2286, info.height);
done();
});
});
it('Do not resize when both withoutEnlargement and withoutReduction are true', function (done) {
sharp(fixtures.inputJpg)
.resize(320, 320, { fit: 'fill', withoutEnlargement: true, withoutReduction: true })
.toBuffer(function (err, data, info) {
if (err) throw err;
assert.strictEqual(true, data.length > 0);
assert.strictEqual('jpeg', info.format);
assert.strictEqual(2725, info.width);
assert.strictEqual(2225, info.height);
done();
});
});
it('Do not reduce size when fit = outside and withoutReduction are true and height > outputHeight and width > outputWidth', function (done) {
sharp(fixtures.inputJpg)
.resize(320, 320, { fit: 'outside', withoutReduction: true })
.toBuffer(function (err, data, info) {
if (err) throw err;
assert.strictEqual(true, data.length > 0);
assert.strictEqual('jpeg', info.format);
assert.strictEqual(2725, info.width);
assert.strictEqual(2225, info.height);
done();
});
});
it('Do resize when fit = outside and withoutReduction are true and input height > height and input width > width ', function (done) {
sharp(fixtures.inputJpg)
.resize(3000, 3000, { fit: 'outside', withoutReduction: true })
.toBuffer(function (err, data, info) {
if (err) throw err;
assert.strictEqual(true, data.length > 0);
assert.strictEqual('jpeg', info.format);
assert.strictEqual(3674, info.width);
assert.strictEqual(3000, info.height);
done();
});
});
it('fit=fill, downscale width and height', function (done) { it('fit=fill, downscale width and height', function (done) {
sharp(fixtures.inputJpg) sharp(fixtures.inputJpg)
.resize(320, 320, { fit: 'fill' }) .resize(320, 320, { fit: 'fill' })