diff --git a/docs/api-output.md b/docs/api-output.md index 3fa0dc21..083ad80d 100644 --- a/docs/api-output.md +++ b/docs/api-output.md @@ -372,6 +372,7 @@ Warning: multiple sharp instances concurrently producing tile output can expose - `options.skipBlanks` **[number][9]** threshold to skip tile generation, a value 0 - 255 for 8-bit images or 0 - 65535 for 16-bit images (optional, default `-1`) - `options.container` **[string][2]** tile container, with value `fs` (filesystem) or `zip` (compressed file). (optional, default `'fs'`) - `options.layout` **[string][2]** filesystem layout, possible values are `dz`, `iiif`, `zoomify` or `google`. (optional, default `'dz'`) + - `options.centre|center` **[boolean][7]** center image in tile (optional, default `false`) ### Examples diff --git a/lib/constructor.js b/lib/constructor.js index e1691889..becc49b9 100644 --- a/lib/constructor.js +++ b/lib/constructor.js @@ -238,6 +238,7 @@ const Sharp = function (input, options) { tileAngle: 0, tileSkipBlanks: -1, tileBackground: [255, 255, 255, 255], + tileCentre: false, linearA: 1, linearB: 0, // Function to notify of libvips warnings diff --git a/lib/output.js b/lib/output.js index 13d349e0..8ec94873 100644 --- a/lib/output.js +++ b/lib/output.js @@ -658,6 +658,7 @@ function raw () { * @param {number} [options.skipBlanks=-1] threshold to skip tile generation, a value 0 - 255 for 8-bit images or 0 - 65535 for 16-bit images * @param {string} [options.container='fs'] tile container, with value `fs` (filesystem) or `zip` (compressed file). * @param {string} [options.layout='dz'] filesystem layout, possible values are `dz`, `iiif`, `zoomify` or `google`. + * @param {boolean} [options.centre=false] center image in tile. * @returns {Sharp} * @throws {Error} Invalid parameters */ @@ -726,6 +727,10 @@ function tile (options) { } else if (is.defined(options.layout) && options.layout === 'google') { this.options.tileSkipBlanks = 5; } + // Center image in tile + if (is.defined(options.centre) || is.defined(options.center)) { + this._setBooleanOption('tileCentre', options.centre || options.center); + } } // Format if (is.inArray(this.options.formatOut, ['jpeg', 'png', 'webp'])) { diff --git a/src/pipeline.cc b/src/pipeline.cc index 3081c336..b182d7bb 100644 --- a/src/pipeline.cc +++ b/src/pipeline.cc @@ -1014,6 +1014,7 @@ class PipelineWorker : public Napi::AsyncWorker { ->set("suffix", const_cast(suffix.data())) ->set("angle", CalculateAngleRotation(baton->tileAngle)) ->set("background", baton->tileBackground) + ->set("centre", baton->tileCentre) ->set("skip_blanks", baton->tileSkipBlanks); // libvips chooses a default depth based on layout. Instead of replicating that logic here by // not passing anything - libvips will handle choice @@ -1403,6 +1404,7 @@ Napi::Value pipeline(const Napi::CallbackInfo& info) { baton->tileDepth = static_cast( vips_enum_from_nick(nullptr, VIPS_TYPE_FOREIGN_DZ_DEPTH, sharp::AttrAsStr(options, "tileDepth").data())); + baton->tileCentre = sharp::AttrAsBool(options, "tileCentre"); // Force random access for certain operations if (baton->input->access == VIPS_ACCESS_SEQUENTIAL) { diff --git a/src/pipeline.h b/src/pipeline.h index ef2d668d..f4ebf6f1 100644 --- a/src/pipeline.h +++ b/src/pipeline.h @@ -79,6 +79,7 @@ struct PipelineBaton { int cropOffsetLeft; int cropOffsetTop; bool premultiplied; + bool tileCentre; std::string kernel; bool fastShrinkOnLoad; double tintA; diff --git a/test/fixtures/expected/tile_centered.jpg b/test/fixtures/expected/tile_centered.jpg new file mode 100644 index 00000000..9ae14ff6 Binary files /dev/null and b/test/fixtures/expected/tile_centered.jpg differ diff --git a/test/unit/tile.js b/test/unit/tile.js index 520aca48..4868a0e6 100644 --- a/test/unit/tile.js +++ b/test/unit/tile.js @@ -289,6 +289,14 @@ describe('Tile', function () { }); }); + it('Invalid center parameter value fail', function () { + assert.throws(function () { + sharp().tile({ + center: 'true' + }); + }); + }); + it('Deep Zoom layout', function (done) { const directory = fixtures.path('output.dzi_files'); rimraf(directory, function () { @@ -765,6 +773,46 @@ describe('Tile', function () { }); }); + it('Google layout with center image in tile', function (done) { + const directory = fixtures.path('output.google_center.dzi'); + rimraf(directory, function () { + sharp(fixtures.inputJpg) + .tile({ + center: true, + layout: 'google' + }) + .toFile(directory, function (err, info) { + if (err) throw err; + assert.strictEqual('dz', info.format); + assert.strictEqual(2725, info.width); + assert.strictEqual(2225, info.height); + assert.strictEqual(3, info.channels); + assert.strictEqual('number', typeof info.size); + fixtures.assertSimilar(fixtures.expected('tile_centered.jpg'), fs.readFileSync(path.join(directory, '0', '0', '0.jpg')), done); + }); + }); + }); + + it('Google layout with center image in tile centre', function (done) { + const directory = fixtures.path('output.google_center.dzi'); + rimraf(directory, function () { + sharp(fixtures.inputJpg) + .tile({ + centre: true, + layout: 'google' + }) + .toFile(directory, function (err, info) { + if (err) throw err; + assert.strictEqual('dz', info.format); + assert.strictEqual(2725, info.width); + assert.strictEqual(2225, info.height); + assert.strictEqual(3, info.channels); + assert.strictEqual('number', typeof info.size); + fixtures.assertSimilar(fixtures.expected('tile_centered.jpg'), fs.readFileSync(path.join(directory, '0', '0', '0.jpg')), done); + }); + }); + }); + it('IIIF layout', function (done) { const directory = fixtures.path('output.iiif.info'); rimraf(directory, function () {