diff --git a/docs/api-resize.md b/docs/api-resize.md index d4059470..970bee4f 100644 --- a/docs/api-resize.md +++ b/docs/api-resize.md @@ -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.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.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`) ### 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 const scaleByHalf = await sharp(input) .metadata() diff --git a/lib/constructor.js b/lib/constructor.js index eb25abfd..e644f96f 100644 --- a/lib/constructor.js +++ b/lib/constructor.js @@ -166,6 +166,7 @@ const Sharp = function (input, options) { extendRight: 0, extendBackground: [0, 0, 0, 255], withoutEnlargement: false, + withoutReduction: false, affineMatrix: [], affineBackground: [0, 0, 0, 255], affineIdx: 0, diff --git a/lib/resize.js b/lib/resize.js index 9843910f..fd69535f 100644 --- a/lib/resize.js +++ b/lib/resize.js @@ -183,6 +183,20 @@ function isRotationExpected (options) { * }); * * @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) * .metadata() * .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} [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.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. * @returns {Sharp} * @throws {Error} Invalid parameters @@ -276,6 +291,10 @@ function resize (width, height, options) { if (is.defined(options.withoutEnlargement)) { this._setBooleanOption('withoutEnlargement', options.withoutEnlargement); } + // Without reduction + if (is.defined(options.withoutReduction)) { + this._setBooleanOption('withoutReduction', options.withoutReduction); + } // Shrink on load if (is.defined(options.fastShrinkOnLoad)) { this._setBooleanOption('fastShrinkOnLoad', options.fastShrinkOnLoad); diff --git a/package.json b/package.json index 79081337..56b3a135 100644 --- a/package.json +++ b/package.json @@ -79,7 +79,8 @@ "Michael Nutt ", "Brad Parham ", "Taneli Vatanen ", - "Joris Dugué " + "Joris Dugué ", + "Chris Banks " ], "scripts": { "install": "(node install/libvips && node install/dll-copy && prebuild-install) || (node install/can-compile && node-gyp rebuild && node install/dll-copy)", diff --git a/src/pipeline.cc b/src/pipeline.cc index 6f51da18..b88d8944 100644 --- a/src/pipeline.cc +++ b/src/pipeline.cc @@ -130,6 +130,17 @@ class PipelineWorker : public Napi::AsyncWorker { 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 double hshrink; double vshrink; @@ -1356,6 +1367,7 @@ Napi::Value pipeline(const Napi::CallbackInfo& info) { } // Resize options baton->withoutEnlargement = sharp::AttrAsBool(options, "withoutEnlargement"); + baton->withoutReduction = sharp::AttrAsBool(options, "withoutReduction"); baton->position = sharp::AttrAsInt32(options, "position"); baton->resizeBackground = sharp::AttrAsVectorOfDouble(options, "resizeBackground"); baton->kernel = sharp::AttrAsStr(options, "kernel"); diff --git a/src/pipeline.h b/src/pipeline.h index 4f048ced..b0cca2b1 100644 --- a/src/pipeline.h +++ b/src/pipeline.h @@ -119,6 +119,7 @@ struct PipelineBaton { int extendRight; std::vector extendBackground; bool withoutEnlargement; + bool withoutReduction; std::vector affineMatrix; std::vector affineBackground; double affineIdx; @@ -259,6 +260,7 @@ struct PipelineBaton { extendRight(0), extendBackground{ 0.0, 0.0, 0.0, 255.0 }, withoutEnlargement(false), + withoutReduction(false), affineMatrix{ 1.0, 0.0, 0.0, 1.0 }, affineBackground{ 0.0, 0.0, 0.0, 255.0 }, affineIdx(0), diff --git a/test/unit/resize.js b/test/unit/resize.js index 364d8a50..fa0eb66b 100644 --- a/test/unit/resize.js +++ b/test/unit/resize.js @@ -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) { sharp(fixtures.inputJpg) .resize(320, 320, { fit: 'fill' })