From 99118634414fded871f679ee9923208579d19b93 Mon Sep 17 00:00:00 2001 From: Lovell Fuller Date: Fri, 4 Nov 2016 18:16:16 +0000 Subject: [PATCH] Expose libvips centre option, mimics *magick +0.5px convention --- docs/api-output.md | 1 + docs/api-resize.md | 2 + docs/changelog.md | 3 + lib/constructor.js | 1 + lib/output.js | 4 +- lib/resize.js | 7 + src/pipeline.cc | 11 +- src/pipeline.h | 2 + test/unit/resize.js | 333 +++++++++++++++++++++++--------------------- 9 files changed, 201 insertions(+), 163 deletions(-) diff --git a/docs/api-output.md b/docs/api-output.md index a7c0209b..903e54ce 100644 --- a/docs/api-output.md +++ b/docs/api-output.md @@ -68,6 +68,7 @@ Use these JPEG options for output image. - `trellisQuantisation` **\[[Boolean](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Boolean)]** apply trellis quantisation, requires mozjpeg (optional, default `false`) - `overshootDeringing` **\[[Boolean](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Boolean)]** apply overshoot deringing, requires mozjpeg (optional, default `false`) - `optimiseScans` **\[[Boolean](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Boolean)]** optimise progressive scans, forces progressive, requires mozjpeg (optional, default `false`) +- `optimizeScans` **\[[Boolean](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Boolean)]** alternative spelling of optimiseScans (optional, default `false`) - Throws **[Error](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error)** Invalid options diff --git a/docs/api-resize.md b/docs/api-resize.md index f69e45e1..8b54ccf4 100644 --- a/docs/api-resize.md +++ b/docs/api-resize.md @@ -27,6 +27,8 @@ Possible enlargement interpolators are: - `options` **\[[Object](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object)]** - `options.kernel` **\[[String](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String)]** the kernel to use for image reduction. (optional, default `'lanczos3'`) - `options.interpolator` **\[[String](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String)]** the interpolator to use for image enlargement. (optional, default `'bicubic'`) + - `options.centreSampling` **\[[Boolean](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Boolean)]** use \*magick centre sampling convention instead of corner sampling. (optional, default `false`) + - `options.centerSampling` **\[[Boolean](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Boolean)]** alternative spelling of centreSampling. (optional, default `false`) **Examples** diff --git a/docs/changelog.md b/docs/changelog.md index e5912762..4afd0a62 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -15,6 +15,9 @@ Requires libvips v8.4.2. Access to these is now via output format functions, for example `quality(n)` is now `jpeg({quality: n})` and/or `webp({quality: n})`. +* Expose libvips' "centre" resize option to mimic \*magick's +0.5px convention. + [#568](https://github.com/lovell/sharp/issues/568) + * Ensure support for embedded base64 PNG and JPEG images within an SVG. [#601](https://github.com/lovell/sharp/issues/601) [@dynamite-ready](https://github.com/dynamite-ready) diff --git a/lib/constructor.js b/lib/constructor.js index 3dca93e2..b9066d38 100644 --- a/lib/constructor.js +++ b/lib/constructor.js @@ -102,6 +102,7 @@ const Sharp = function (input, options) { withoutEnlargement: false, kernel: 'lanczos3', interpolator: 'bicubic', + centreSampling: false, // operations background: [0, 0, 0, 255], flatten: false, diff --git a/lib/output.js b/lib/output.js index 866488b7..b346241f 100644 --- a/lib/output.js +++ b/lib/output.js @@ -115,14 +115,14 @@ const jpeg = function jpeg (options) { throw new Error('Invalid chromaSubsampling (4:2:0, 4:4:4) ' + options.chromaSubsampling); } } - options.trellisQuantisation = options.trellisQuantisation || options.trellisQuantization; + options.trellisQuantisation = is.bool(options.trellisQuantization) ? options.trellisQuantization : options.trellisQuantisation; if (is.defined(options.trellisQuantisation)) { this._setBooleanOption('jpegTrellisQuantisation', options.trellisQuantisation); } if (is.defined(options.overshootDeringing)) { this._setBooleanOption('jpegOvershootDeringing', options.overshootDeringing); } - options.optimiseScans = is.bool(options.optimiseScans) ? options.optimiseScans : options.optimizeScans; + options.optimiseScans = is.bool(options.optimizeScans) ? options.optimizeScans : options.optimiseScans; if (is.defined(options.optimiseScans)) { this._setBooleanOption('jpegOptimiseScans', options.optimiseScans); if (options.optimiseScans) { diff --git a/lib/resize.js b/lib/resize.js index d049bbff..4f8318e2 100644 --- a/lib/resize.js +++ b/lib/resize.js @@ -94,6 +94,8 @@ const interpolator = { * @param {Object} [options] * @param {String} [options.kernel='lanczos3'] - the kernel to use for image reduction. * @param {String} [options.interpolator='bicubic'] - the interpolator to use for image enlargement. + * @param {Boolean} [options.centreSampling=false] - use *magick centre sampling convention instead of corner sampling. + * @param {Boolean} [options.centerSampling=false] - alternative spelling of centreSampling. * @returns {Sharp} * @throws {Error} Invalid parameters */ @@ -133,6 +135,11 @@ const resize = function resize (width, height, options) { throw new Error('Invalid interpolator ' + options.interpolator); } } + // Centre sampling + options.centreSampling = is.bool(options.centerSampling) ? options.centerSampling : options.centreSampling; + if (is.defined(options.centreSampling)) { + this._setBooleanOption('centreSampling', options.centreSampling); + } } return this; }; diff --git a/src/pipeline.cc b/src/pipeline.cc index bf571452..0e686b14 100644 --- a/src/pipeline.cc +++ b/src/pipeline.cc @@ -388,10 +388,16 @@ class PipelineWorker : public Nan::AsyncWorker { throw vips::VError("Unknown kernel"); } if (yresidual < 1.0) { - image = image.reducev(1.0 / yresidual, VImage::option()->set("kernel", kernel)); + image = image.reducev(1.0 / yresidual, VImage::option() + ->set("kernel", kernel) + ->set("centre", baton->centreSampling) + ); } if (xresidual < 1.0) { - image = image.reduceh(1.0 / xresidual, VImage::option()->set("kernel", kernel)); + image = image.reduceh(1.0 / xresidual, VImage::option() + ->set("kernel", kernel) + ->set("centre", baton->centreSampling) + ); } } // Perform affine enlargement @@ -1063,6 +1069,7 @@ NAN_METHOD(pipeline) { baton->crop = AttrTo(options, "crop"); baton->kernel = AttrAsStr(options, "kernel"); baton->interpolator = AttrAsStr(options, "interpolator"); + baton->centreSampling = AttrTo(options, "centreSampling"); // Join Channel Options if(HasAttr(options, "joinChannelIn")) { v8::Local joinChannelObject = Nan::Get(options, Nan::New("joinChannelIn").ToLocalChecked()) diff --git a/src/pipeline.h b/src/pipeline.h index 64e471cb..2bc8536c 100644 --- a/src/pipeline.h +++ b/src/pipeline.h @@ -50,6 +50,7 @@ struct PipelineBaton { int cropCalcTop; std::string kernel; std::string interpolator; + bool centreSampling; double background[4]; bool flatten; bool negate; @@ -119,6 +120,7 @@ struct PipelineBaton { crop(0), cropCalcLeft(-1), cropCalcTop(-1), + centreSampling(false), flatten(false), negate(false), blurSigma(0.0), diff --git a/test/unit/resize.js b/test/unit/resize.js index 38704703..5342d62c 100644 --- a/test/unit/resize.js +++ b/test/unit/resize.js @@ -51,137 +51,110 @@ describe('Resize dimensions', function () { }); it('Upscale', function (done) { - sharp(fixtures.inputJpg).resize(3000).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(2450, info.height); - done(); + sharp(fixtures.inputJpg) + .resize(3000) + .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(2450, info.height); + done(); + }); + }); + + it('Invalid width - NaN', function () { + assert.throws(function () { + sharp().resize('spoons', 240); }); }); - it('Invalid width - NaN', function (done) { - let isValid = true; - try { - sharp(fixtures.inputJpg).resize('spoons', 240); - } catch (err) { - isValid = false; - } - assert.strictEqual(false, isValid); - done(); - }); - - it('Invalid height - NaN', function (done) { - let isValid = true; - try { - sharp(fixtures.inputJpg).resize(320, 'spoons'); - } catch (err) { - isValid = false; - } - assert.strictEqual(false, isValid); - done(); - }); - - it('Invalid width - float', function (done) { - let isValid = true; - try { - sharp(fixtures.inputJpg).resize(1.5, 240); - } catch (err) { - isValid = false; - } - assert.strictEqual(false, isValid); - done(); - }); - - it('Invalid height - float', function (done) { - let isValid = true; - try { - sharp(fixtures.inputJpg).resize(320, 1.5); - } catch (err) { - isValid = false; - } - assert.strictEqual(false, isValid); - done(); - }); - - it('Invalid width - too large', function (done) { - let isValid = true; - try { - sharp(fixtures.inputJpg).resize(0x4000, 240); - } catch (err) { - isValid = false; - } - assert.strictEqual(false, isValid); - done(); - }); - - it('Invalid height - too large', function (done) { - let isValid = true; - try { - sharp(fixtures.inputJpg).resize(320, 0x4000); - } catch (err) { - isValid = false; - } - assert.strictEqual(false, isValid); - done(); - }); - - if (sharp.format.webp.output.buffer) { - it('WebP shrink-on-load rounds to zero, ensure recalculation is correct', function (done) { - sharp(fixtures.inputJpg) - .resize(1080, 607) - .webp() - .toBuffer(function (err, data, info) { - if (err) throw err; - assert.strictEqual('webp', info.format); - assert.strictEqual(1080, info.width); - assert.strictEqual(607, info.height); - sharp(data) - .resize(233, 131) - .toBuffer(function (err, data, info) { - if (err) throw err; - assert.strictEqual('webp', info.format); - assert.strictEqual(233, info.width); - assert.strictEqual(131, info.height); - done(); - }); - }); + it('Invalid height - NaN', function () { + assert.throws(function () { + sharp().resize(320, 'spoons'); }); - } + }); - if (sharp.format.tiff.input.file) { - it('TIFF embed known to cause rounding errors', function (done) { - sharp(fixtures.inputTiff) - .resize(240, 320) - .embed() - .jpeg() - .toBuffer(function (err, data, info) { - if (err) throw err; - assert.strictEqual(true, data.length > 0); - assert.strictEqual('jpeg', info.format); - assert.strictEqual(240, info.width); - assert.strictEqual(320, info.height); - done(); - }); + it('Invalid width - float', function () { + assert.throws(function () { + sharp().resize(1.5, 240); }); + }); - it('TIFF known to cause rounding errors', function (done) { - sharp(fixtures.inputTiff) - .resize(240, 320) - .jpeg() - .toBuffer(function (err, data, info) { - if (err) throw err; - assert.strictEqual(true, data.length > 0); - assert.strictEqual('jpeg', info.format); - assert.strictEqual(240, info.width); - assert.strictEqual(320, info.height); - done(); - }); + it('Invalid height - float', function () { + assert.throws(function () { + sharp().resize(320, 1.5); }); + }); - it('Max width or height considering ratio (portrait)', function (done) { - sharp(fixtures.inputTiff).resize(320, 320).max().jpeg().toBuffer(function (err, data, info) { + it('Invalid width - too large', function () { + assert.throws(function () { + sharp().resize(0x4000, 240); + }); + }); + + it('Invalid height - too large', function () { + assert.throws(function () { + sharp().resize(320, 0x4000); + }); + }); + + it('WebP shrink-on-load rounds to zero, ensure recalculation is correct', function (done) { + sharp(fixtures.inputJpg) + .resize(1080, 607) + .webp() + .toBuffer(function (err, data, info) { + if (err) throw err; + assert.strictEqual('webp', info.format); + assert.strictEqual(1080, info.width); + assert.strictEqual(607, info.height); + sharp(data) + .resize(233, 131) + .toBuffer(function (err, data, info) { + if (err) throw err; + assert.strictEqual('webp', info.format); + assert.strictEqual(233, info.width); + assert.strictEqual(131, info.height); + done(); + }); + }); + }); + + it('TIFF embed known to cause rounding errors', function (done) { + sharp(fixtures.inputTiff) + .resize(240, 320) + .embed() + .jpeg() + .toBuffer(function (err, data, info) { + if (err) throw err; + assert.strictEqual(true, data.length > 0); + assert.strictEqual('jpeg', info.format); + assert.strictEqual(240, info.width); + assert.strictEqual(320, info.height); + done(); + }); + }); + + it('TIFF known to cause rounding errors', function (done) { + sharp(fixtures.inputTiff) + .resize(240, 320) + .jpeg() + .toBuffer(function (err, data, info) { + if (err) throw err; + assert.strictEqual(true, data.length > 0); + assert.strictEqual('jpeg', info.format); + assert.strictEqual(240, info.width); + assert.strictEqual(320, info.height); + done(); + }); + }); + + it('Max width or height considering ratio (portrait)', function (done) { + sharp(fixtures.inputTiff) + .resize(320, 320) + .max() + .jpeg() + .toBuffer(function (err, data, info) { if (err) throw err; assert.strictEqual(true, data.length > 0); assert.strictEqual('jpeg', info.format); @@ -189,10 +162,14 @@ describe('Resize dimensions', function () { assert.strictEqual(320, info.height); done(); }); - }); + }); - it('Min width or height considering ratio (portrait)', function (done) { - sharp(fixtures.inputTiff).resize(320, 320).min().jpeg().toBuffer(function (err, data, info) { + it('Min width or height considering ratio (portrait)', function (done) { + sharp(fixtures.inputTiff) + .resize(320, 320) + .min() + .jpeg() + .toBuffer(function (err, data, info) { if (err) throw err; assert.strictEqual(true, data.length > 0); assert.strictEqual('jpeg', info.format); @@ -200,51 +177,62 @@ describe('Resize dimensions', function () { assert.strictEqual(422, info.height); done(); }); - }); - } + }); it('Max width or height considering ratio (landscape)', function (done) { - sharp(fixtures.inputJpg).resize(320, 320).max().toBuffer(function (err, data, info) { - if (err) throw err; - assert.strictEqual(true, data.length > 0); - assert.strictEqual('jpeg', info.format); - assert.strictEqual(320, info.width); - assert.strictEqual(261, info.height); - done(); - }); + sharp(fixtures.inputJpg) + .resize(320, 320) + .max() + .toBuffer(function (err, data, info) { + if (err) throw err; + assert.strictEqual(true, data.length > 0); + assert.strictEqual('jpeg', info.format); + assert.strictEqual(320, info.width); + assert.strictEqual(261, info.height); + done(); + }); }); it('Provide only one dimension with max, should default to crop', function (done) { - sharp(fixtures.inputJpg).resize(320).max().toBuffer(function (err, data, info) { - if (err) throw err; - assert.strictEqual(true, data.length > 0); - assert.strictEqual('jpeg', info.format); - assert.strictEqual(320, info.width); - assert.strictEqual(261, info.height); - done(); - }); + sharp(fixtures.inputJpg) + .resize(320) + .max() + .toBuffer(function (err, data, info) { + if (err) throw err; + assert.strictEqual(true, data.length > 0); + assert.strictEqual('jpeg', info.format); + assert.strictEqual(320, info.width); + assert.strictEqual(261, info.height); + done(); + }); }); it('Min width or height considering ratio (landscape)', function (done) { - sharp(fixtures.inputJpg).resize(320, 320).min().toBuffer(function (err, data, info) { - if (err) throw err; - assert.strictEqual(true, data.length > 0); - assert.strictEqual('jpeg', info.format); - assert.strictEqual(392, info.width); - assert.strictEqual(320, info.height); - done(); - }); + sharp(fixtures.inputJpg) + .resize(320, 320) + .min() + .toBuffer(function (err, data, info) { + if (err) throw err; + assert.strictEqual(true, data.length > 0); + assert.strictEqual('jpeg', info.format); + assert.strictEqual(392, info.width); + assert.strictEqual(320, info.height); + done(); + }); }); it('Provide only one dimension with min, should default to crop', function (done) { - sharp(fixtures.inputJpg).resize(320).min().toBuffer(function (err, data, info) { - if (err) throw err; - assert.strictEqual(true, data.length > 0); - assert.strictEqual('jpeg', info.format); - assert.strictEqual(320, info.width); - assert.strictEqual(261, info.height); - done(); - }); + sharp(fixtures.inputJpg) + .resize(320) + .min() + .toBuffer(function (err, data, info) { + if (err) throw err; + assert.strictEqual(true, data.length > 0); + assert.strictEqual('jpeg', info.format); + assert.strictEqual(320, info.width); + assert.strictEqual(261, info.height); + done(); + }); }); it('Do not enlarge when input width is already less than output width', function (done) { @@ -387,4 +375,31 @@ describe('Resize dimensions', function () { done(); }); }); + + it('Centre vs corner convention return different results', function (done) { + sharp(fixtures.inputJpg) + .resize(32, 24, { centreSampling: false }) + .greyscale() + .raw() + .toBuffer(function (err, cornerData) { + if (err) throw err; + assert.strictEqual(768, cornerData.length); + sharp(fixtures.inputJpg) + .resize(32, 24, { centerSampling: true }) + .greyscale() + .raw() + .toBuffer(function (err, centreData) { + if (err) throw err; + assert.strictEqual(768, centreData.length); + assert.notStrictEqual(0, cornerData.compare(centreData)); + done(); + }); + }); + }); + + it('Invalid centreSampling option', function () { + assert.throws(function () { + sharp().resize(32, 24, { centreSampling: 1 }); + }); + }); });