From f86ae79fdb651a83b1e4e664312b1aaf8d7708c6 Mon Sep 17 00:00:00 2001 From: Andrea Bianco Date: Sun, 18 Feb 2018 20:00:08 +0100 Subject: [PATCH] Expose angle option in tile feature (#1121) --- lib/output.js | 11 ++++++ src/pipeline.cc | 4 ++- src/pipeline.h | 4 ++- test/unit/tile.js | 91 ++++++++++++++++++++++++++++++++++++++++++----- 4 files changed, 99 insertions(+), 11 deletions(-) diff --git a/lib/output.js b/lib/output.js index e24926fc..e5a19696 100644 --- a/lib/output.js +++ b/lib/output.js @@ -319,6 +319,7 @@ function toFormat (format, options) { * @param {Object} [tile] * @param {Number} [tile.size=256] tile size in pixels, a value between 1 and 8192. * @param {Number} [tile.overlap=0] tile overlap in pixels, a value between 0 and 8192. + * @param {Number} [tile.angle=0] tile, angle of rotation, must be a multiple of 90.. * @param {String} [tile.container='fs'] tile container, with value `fs` (filesystem) or `zip` (compressed file). * @param {String} [tile.layout='dz'] filesystem layout, possible values are `dz`, `zoomify` or `google`. * @returns {Sharp} @@ -361,6 +362,15 @@ function tile (tile) { throw new Error('Invalid tile layout ' + tile.layout); } } + + // Angle of rotation, + if (is.defined(tile.angle)) { + if (is.integer(tile.angle) && !(tile.angle % 90)) { + this.options.tileAngle = tile.angle; + } else { + throw new Error('Unsupported angle: angle must be a positive/negative multiple of 90 ' + tile.angle); + } + } } // Format if (is.inArray(this.options.formatOut, ['jpeg', 'png', 'webp'])) { @@ -368,6 +378,7 @@ function tile (tile) { } else if (this.options.formatOut !== 'input') { throw new Error('Invalid tile format ' + this.options.formatOut); } + return this._updateFormatOut('dz'); } diff --git a/src/pipeline.cc b/src/pipeline.cc index 74e8a3b6..bd2cf6d5 100644 --- a/src/pipeline.cc +++ b/src/pipeline.cc @@ -917,7 +917,8 @@ class PipelineWorker : public Nan::AsyncWorker { ->set("overlap", baton->tileOverlap) ->set("container", baton->tileContainer) ->set("layout", baton->tileLayout) - ->set("suffix", const_cast(suffix.data()))); + ->set("suffix", const_cast(suffix.data())) + ->set("angle", CalculateAngleRotation(baton->tileAngle))); baton->formatOut = "dz"; } else if (baton->formatOut == "v" || (mightMatchInput && isV) || (willMatchInput && inputImageType == ImageType::VIPS)) { @@ -1263,6 +1264,7 @@ NAN_METHOD(pipeline) { baton->tileSize = AttrTo(options, "tileSize"); baton->tileOverlap = AttrTo(options, "tileOverlap"); std::string tileContainer = AttrAsStr(options, "tileContainer"); + baton->tileAngle = AttrTo(options, "tileAngle"); if (tileContainer == "zip") { baton->tileContainer = VIPS_FOREIGN_DZ_CONTAINER_ZIP; } else { diff --git a/src/pipeline.h b/src/pipeline.h index a93c6b9f..386155e8 100644 --- a/src/pipeline.h +++ b/src/pipeline.h @@ -132,6 +132,7 @@ struct PipelineBaton { VipsForeignDzContainer tileContainer; VipsForeignDzLayout tileLayout; std::string tileFormat; + int tileAngle; PipelineBaton(): input(nullptr), @@ -206,7 +207,8 @@ struct PipelineBaton { tileSize(256), tileOverlap(0), tileContainer(VIPS_FOREIGN_DZ_CONTAINER_FS), - tileLayout(VIPS_FOREIGN_DZ_LAYOUT_DZ) { + tileLayout(VIPS_FOREIGN_DZ_LAYOUT_DZ), + tileAngle(0){ background[0] = 0.0; background[1] = 0.0; background[2] = 0.0; diff --git a/test/unit/tile.js b/test/unit/tile.js index 7af2736e..7fd138c0 100644 --- a/test/unit/tile.js +++ b/test/unit/tile.js @@ -146,13 +146,38 @@ describe('Tile', function () { it('Prevent larger overlap than default size', function () { assert.throws(function () { - sharp().tile({overlap: 257}); + sharp().tile({ + overlap: 257 + }); }); }); it('Prevent larger overlap than provided size', function () { assert.throws(function () { - sharp().tile({size: 512, overlap: 513}); + sharp().tile({ + size: 512, + overlap: 513 + }); + }); + }); + + it('Valid rotation angle values pass', function () { + [90, 270, -90].forEach(function (angle) { + assert.doesNotThrow(function () { + sharp().tile({ + angle: angle + }); + }); + }); + }); + + it('Invalid rotation angle values fail', function () { + ['zoinks', 1.1, -1, 27].forEach(function (angle) { + assert.throws(function () { + sharp().tile({ + angle: angle + }); + }); }); }); @@ -192,6 +217,40 @@ describe('Tile', function () { }); }); + it('Deep Zoom layout with custom size+angle', function (done) { + const directory = fixtures.path('output.512_90.dzi_files'); + rimraf(directory, function () { + sharp(fixtures.inputJpg) + .tile({ + size: 512, + angle: 90 + }) + .toFile(fixtures.path('output.512_90.dzi'), 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('undefined', typeof info.size); + assertDeepZoomTiles(directory, 512, 13, done); + // Verifies tiles in 10th level are rotated + let tile = path.join(directory, '10', '0_1.jpeg'); + // verify that the width and height correspond to the rotated image + // expected are w=512 and h=170 for the 0_1.jpeg. + // if a 0 angle is supplied to the .tile function + // the expected values are w=170 and h=512 for the 1_0.jpeg + sharp(tile).metadata(function (err, metadata) { + if (err) { + throw err; + } else { + assert.strictEqual(true, metadata.width === 512); + assert.strictEqual(true, metadata.height === 170); + } + }); + }); + }); + }); + it('Zoomify layout', function (done) { const directory = fixtures.path('output.zoomify.dzi'); rimraf(directory, function () { @@ -244,7 +303,9 @@ describe('Tile', function () { const directory = fixtures.path('output.jpg.google.dzi'); rimraf(directory, function () { sharp(fixtures.inputJpg) - .jpeg({ quality: 1 }) + .jpeg({ + quality: 1 + }) .tile({ layout: 'google' }) @@ -279,7 +340,9 @@ describe('Tile', function () { const directory = fixtures.path('output.png.google.dzi'); rimraf(directory, function () { sharp(fixtures.inputJpg) - .png({ compressionLevel: 1 }) + .png({ + compressionLevel: 1 + }) .tile({ layout: 'google' }) @@ -314,7 +377,9 @@ describe('Tile', function () { const directory = fixtures.path('output.webp.google.dzi'); rimraf(directory, function () { sharp(fixtures.inputJpg) - .webp({ quality: 1 }) + .webp({ + quality: 1 + }) .tile({ layout: 'google' }) @@ -363,8 +428,12 @@ describe('Tile', function () { assert.strictEqual(true, stat.isFile()); assert.strictEqual(true, stat.size > 0); fs.createReadStream(container) - .pipe(unzip.Extract({path: path.dirname(extractTo)})) - .on('error', function (err) { throw err; }) + .pipe(unzip.Extract({ + path: path.dirname(extractTo) + })) + .on('error', function (err) { + throw err; + }) .on('close', function () { assertDeepZoomTiles(directory, 256, 13, done); }); @@ -395,8 +464,12 @@ describe('Tile', function () { assert.strictEqual(true, stat.isFile()); assert.strictEqual(true, stat.size > 0); fs.createReadStream(container) - .pipe(unzip.Extract({path: path.dirname(extractTo)})) - .on('error', function (err) { throw err; }) + .pipe(unzip.Extract({ + path: path.dirname(extractTo) + })) + .on('error', function (err) { + throw err; + }) .on('close', function () { assertDeepZoomTiles(directory, 256, 13, done); });