diff --git a/docs/changelog.md b/docs/changelog.md index 3b8459ce..92cc299b 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -16,6 +16,10 @@ Requires libvips v8.10.6 * Reduce concurrency on glibc-based Linux when using the default memory allocator to help prevent fragmentation. +* Ensure `@id` attribute can be set for IIIF tile-based output. + [#2612](https://github.com/lovell/sharp/issues/2612) + [@edsilv](https://github.com/edsilv) + ## v0.27 - *avif* Requires libvips v8.10.5 diff --git a/lib/constructor.js b/lib/constructor.js index dcd5be2e..214f85a6 100644 --- a/lib/constructor.js +++ b/lib/constructor.js @@ -279,6 +279,7 @@ const Sharp = function (input, options) { tileSkipBlanks: -1, tileBackground: [255, 255, 255, 255], tileCentre: false, + tileId: 'https://example.com/iiif', linearA: 1, linearB: 0, // Function to notify of libvips warnings diff --git a/lib/output.js b/lib/output.js index 56de2da4..b1b0170f 100644 --- a/lib/output.js +++ b/lib/output.js @@ -727,6 +727,7 @@ function raw () { * @param {string} [options.layout='dz'] filesystem layout, possible values are `dz`, `iiif`, `zoomify` or `google`. * @param {boolean} [options.centre=false] centre image in tile. * @param {boolean} [options.center=false] alternative spelling of centre. + * @param {string} [options.id='https://example.com/iiif'] when `layout` is `iiif`, sets the `@id` attribute of `info.json` * @returns {Sharp} * @throws {Error} Invalid parameters */ @@ -800,6 +801,14 @@ function tile (options) { if (is.defined(centre)) { this._setBooleanOption('tileCentre', centre); } + // @id attribute for IIIF layout + if (is.defined(options.id)) { + if (is.string(options.id)) { + this.options.tileId = options.id; + } else { + throw is.invalidParameterError('id', 'string', options.id); + } + } } // Format if (is.inArray(this.options.formatOut, ['jpeg', 'png', 'webp'])) { diff --git a/src/pipeline.cc b/src/pipeline.cc index da4b87f6..06280ba2 100644 --- a/src/pipeline.cc +++ b/src/pipeline.cc @@ -1038,6 +1038,7 @@ class PipelineWorker : public Napi::AsyncWorker { ->set("angle", CalculateAngleRotation(baton->tileAngle)) ->set("background", baton->tileBackground) ->set("centre", baton->tileCentre) + ->set("id", const_cast(baton->tileId.data())) ->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 @@ -1438,6 +1439,7 @@ Napi::Value pipeline(const Napi::CallbackInfo& info) { vips_enum_from_nick(nullptr, VIPS_TYPE_FOREIGN_DZ_DEPTH, sharp::AttrAsStr(options, "tileDepth").data())); baton->tileCentre = sharp::AttrAsBool(options, "tileCentre"); + baton->tileId = sharp::AttrAsStr(options, "tileId"); // Force random access for certain operations if (baton->input->access == VIPS_ACCESS_SEQUENTIAL) { diff --git a/src/pipeline.h b/src/pipeline.h index 37fb5e8a..a479bdf4 100644 --- a/src/pipeline.h +++ b/src/pipeline.h @@ -192,6 +192,7 @@ struct PipelineBaton { std::vector tileBackground; int tileSkipBlanks; VipsForeignDzDepth tileDepth; + std::string tileId; std::unique_ptr recombMatrix; PipelineBaton(): diff --git a/test/unit/tile.js b/test/unit/tile.js index eee2732a..171305c9 100644 --- a/test/unit/tile.js +++ b/test/unit/tile.js @@ -297,6 +297,22 @@ describe('Tile', function () { }); }); + it('Valid id parameter value passes', function () { + assert.doesNotThrow(function () { + sharp().tile({ + id: 'test' + }); + }); + }); + + it('Invalid id parameter value fails', function () { + assert.throws(function () { + sharp().tile({ + id: true + }); + }); + }); + it('Deep Zoom layout', function (done) { const directory = fixtures.path('output.dzi_files'); rimraf(directory, function () { @@ -815,11 +831,14 @@ describe('Tile', function () { }); it('IIIF layout', function (done) { - const directory = fixtures.path('output.iiif.info'); + const name = 'output.iiif.info'; + const directory = fixtures.path(name); rimraf(directory, function () { + const id = 'https://sharp.test.com/iiif'; sharp(fixtures.inputJpg) .tile({ - layout: 'iiif' + layout: 'iiif', + id }) .toFile(directory, function (err, info) { if (err) throw err; @@ -828,6 +847,8 @@ describe('Tile', function () { assert.strictEqual(2225, info.height); assert.strictEqual(3, info.channels); assert.strictEqual('number', typeof info.size); + const infoJson = require(path.join(directory, 'info.json')); + assert.strictEqual(`${id}/${name}`, infoJson['@id']); fs.stat(path.join(directory, '0,0,256,256', '256,', '0', 'default.jpg'), function (err, stat) { if (err) throw err; assert.strictEqual(true, stat.isFile());