diff --git a/docs/api.md b/docs/api.md index b8641e6f..cd186926 100644 --- a/docs/api.md +++ b/docs/api.md @@ -510,18 +510,25 @@ This has no effect if the input image does not have an EXIF `Orientation` tag. The default behaviour, when `withMetadata` is not used, is to strip all metadata and convert to the device-independent sRGB colour space. -#### tile([size], [overlap]) +#### tile(options) -The size and overlap, in pixels, of square Deep Zoom image pyramid tiles. +The size, overlap and directory layout to use when generating square Deep Zoom image pyramid tiles. + +`options` is an Object with one or more of the following attributes: * `size` is an integral Number between 1 and 8192. The default value is 256 pixels. * `overlap` is an integral Number between 0 and 8192. The default value is 0 pixels. +* `layout` is a String, with value `dz`, `zoomify` or `google`. The default value is `dz`. ```javascript -sharp('input.tiff').tile(256).toFile('output.dzi', function(err, info) { - // The output.dzi file is the XML format Deep Zoom definition - // The output_files directory contains 256x256 pixel tiles grouped by zoom level -}); +sharp('input.tiff') + .tile({ + size: 512 + }) + .toFile('output.dzi', function(err, info) { + // output.dzi is the Deep Zoom XML definition + // output_files contains 512x512 tiles grouped by zoom level + }); ``` #### withoutChromaSubsampling() diff --git a/docs/changelog.md b/docs/changelog.md index 60b8cac9..4bac9062 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -6,6 +6,10 @@ [#128](https://github.com/lovell/sharp/issues/128) [@blowsie](https://github.com/blowsie) +* Add support for Zoomify and Google tile layouts. Breaks existing tile API. + [#223](https://github.com/lovell/sharp/issues/223) + [@bdunnette](https://github.com/bdunnette) + * Improvements to overlayWith: differing sizes/formats, gravity, buffer input. [#239](https://github.com/lovell/sharp/issues/239) [@chrisriley](https://github.com/chrisriley) diff --git a/index.js b/index.js index 265ad690..65ad0c09 100644 --- a/index.js +++ b/index.js @@ -166,6 +166,9 @@ var isInteger = function(val) { var inRange = function(val, min, max) { return val >= min && val <= max; }; +var contains = function(val, list) { + return list.indexOf(val) !== -1; +}; /* Set input-related options @@ -629,26 +632,36 @@ Sharp.prototype.withMetadata = function(withMetadata) { }; /* - Tile size and overlap for Deep Zoom output + Tile-based deep zoom output options: size, overlap, layout */ -Sharp.prototype.tile = function(size, overlap) { - // Size of square tiles, in pixels - if (typeof size !== 'undefined' && size !== null) { - if (!Number.isNaN(size) && size % 1 === 0 && size >= 1 && size <= 8192) { - this.options.tileSize = size; - } else { - throw new Error('Invalid tile size (1 to 8192) ' + size); - } - } - // Overlap of tiles, in pixels - if (typeof overlap !== 'undefined' && overlap !== null) { - if (!Number.isNaN(overlap) && overlap % 1 === 0 && overlap >= 0 && overlap <= 8192) { - if (overlap > this.options.tileSize) { - throw new Error('Tile overlap ' + overlap + ' cannot be larger than tile size ' + this.options.tileSize); +Sharp.prototype.tile = function(tile) { + if (isObject(tile)) { + // Size of square tiles, in pixels + if (isDefined(tile.size)) { + if (isInteger(tile.size) && inRange(tile.size, 1, 8192)) { + this.options.tileSize = tile.size; + } else { + throw new Error('Invalid tile size (1 to 8192) ' + tile.size); + } + } + // Overlap of tiles, in pixels + if (isDefined(tile.overlap)) { + if (isInteger(tile.overlap) && inRange(tile.overlap, 0, 8192)) { + if (tile.overlap > this.options.tileSize) { + throw new Error('Tile overlap ' + tile.overlap + ' cannot be larger than tile size ' + this.options.tileSize); + } + this.options.tileOverlap = tile.overlap; + } else { + throw new Error('Invalid tile overlap (0 to 8192) ' + tile.overlap); + } + } + // Layout + if (isDefined(tile.layout)) { + if (isString(tile.layout) && contains(tile.layout, ['dz', 'google', 'zoomify'])) { + this.options.tileLayout = tile.layout; + } else { + throw new Error('Invalid tile layout ' + tile.layout); } - this.options.tileOverlap = overlap; - } else { - throw new Error('Invalid tile overlap (0 to 8192) ' + overlap); } } return this; diff --git a/src/pipeline.cc b/src/pipeline.cc index f153559f..a4f3e91d 100644 --- a/src/pipeline.cc +++ b/src/pipeline.cc @@ -766,6 +766,7 @@ class PipelineWorker : public AsyncWorker { ->set("strip", !baton->withMetadata) ->set("tile_size", baton->tileSize) ->set("overlap", baton->tileOverlap) + ->set("layout", baton->tileLayout) ); baton->formatOut = "dz"; } else { @@ -1030,8 +1031,18 @@ NAN_METHOD(pipeline) { // Output baton->formatOut = attrAsStr(options, "formatOut"); baton->fileOut = attrAsStr(options, "fileOut"); + // Tile output baton->tileSize = attrAs(options, "tileSize"); baton->tileOverlap = attrAs(options, "tileOverlap"); + std::string tileLayout = attrAsStr(options, "tileLayout"); + if (tileLayout == "google") { + baton->tileLayout = VIPS_FOREIGN_DZ_LAYOUT_GOOGLE; + } else if (tileLayout == "zoomify") { + baton->tileLayout = VIPS_FOREIGN_DZ_LAYOUT_ZOOMIFY; + } else { + baton->tileLayout = VIPS_FOREIGN_DZ_LAYOUT_DZ; + } + // Function to notify of queue length changes Callback *queueListener = new Callback( Get(options, New("queueListener").ToLocalChecked()).ToLocalChecked().As() diff --git a/src/pipeline.h b/src/pipeline.h index ae024614..4f473bfe 100644 --- a/src/pipeline.h +++ b/src/pipeline.h @@ -1,6 +1,8 @@ #ifndef SRC_PIPELINE_H_ #define SRC_PIPELINE_H_ +#include + #include "nan.h" NAN_METHOD(pipeline); @@ -79,6 +81,7 @@ struct PipelineBaton { int withMetadataOrientation; int tileSize; int tileOverlap; + VipsForeignDzLayout tileLayout; PipelineBaton(): bufferInLength(0), @@ -126,7 +129,8 @@ struct PipelineBaton { withMetadata(false), withMetadataOrientation(-1), tileSize(256), - tileOverlap(0) { + tileOverlap(0), + tileLayout(VIPS_FOREIGN_DZ_LAYOUT_DZ) { 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 6b0c797e..f0dd044c 100644 --- a/test/unit/tile.js +++ b/test/unit/tile.js @@ -47,156 +47,149 @@ var assertDeepZoomTiles = function(directory, expectedSize, expectedLevels, done describe('Tile', function() { - describe('Invalid tile values', function() { - it('size - NaN', function(done) { - var isValid = true; - try { - sharp().tile('zoinks'); - } catch (err) { - isValid = false; - } - assert.strictEqual(false, isValid); - done(); + it('Valid size values pass', function() { + [1, 8192].forEach(function(size) { + assert.doesNotThrow(function() { + sharp().tile({ + size: size + }); + }); }); + }); - it('size - float', function(done) { - var isValid = true; - try { - sharp().tile(1.1); - } catch (err) { - isValid = false; - } - assert.strictEqual(false, isValid); - done(); + it('Invalid size values fail', function() { + ['zoinks', 1.1, -1, 0, 8193].forEach(function(size) { + assert.throws(function() { + sharp().tile({ + size: size + }); + }); }); + }); - it('size - negative', function(done) { - var isValid = true; - try { - sharp().tile(-1); - } catch (err) { - isValid = false; - } - assert.strictEqual(false, isValid); - done(); + it('Valid overlap values pass', function() { + [0, 8192].forEach(function(overlap) { + assert.doesNotThrow(function() { + sharp().tile({ + size: 8192, + overlap: overlap + }); + }); }); + }); - it('size - zero', function(done) { - var isValid = true; - try { - sharp().tile(0); - } catch (err) { - isValid = false; - } - assert.strictEqual(false, isValid); - done(); + it('Invalid overlap values fail', function() { + ['zoinks', 1.1, -1, 8193].forEach(function(overlap) { + assert.throws(function() { + sharp().tile({ + overlap: overlap + }); + }); }); + }); - it('size - too large', function(done) { - var isValid = true; - try { - sharp().tile(8193); - } catch (err) { - isValid = false; - } - assert.strictEqual(false, isValid); - done(); + it('Valid layout values pass', function() { + ['dz', 'google', 'zoomify'].forEach(function(layout) { + assert.doesNotThrow(function() { + sharp().tile({ + layout: layout + }); + }); }); + }); - it('overlap - NaN', function(done) { - var isValid = true; - try { - sharp().tile(null, 'zoinks'); - } catch (err) { - isValid = false; - } - assert.strictEqual(false, isValid); - done(); + it('Invalid layout values fail', function() { + ['zoinks', 1].forEach(function(layout) { + assert.throws(function() { + sharp().tile({ + layout: layout + }); + }); }); + }); - it('overlap - float', function(done) { - var isValid = true; - try { - sharp().tile(null, 1.1); - } catch (err) { - isValid = false; - } - assert.strictEqual(false, isValid); - done(); + it('Prevent larger overlap than default size', function() { + assert.throws(function() { + sharp().tile({overlap: 257}); }); + }); - it('overlap - negative', function(done) { - var isValid = true; - try { - sharp().tile(null, -1); - } catch (err) { - isValid = false; - } - assert.strictEqual(false, isValid); - done(); + it('Prevent larger overlap than provided size', function() { + assert.throws(function() { + sharp().tile({size: 512, overlap: 513}); }); - - it('overlap - too large', function(done) { - var isValid = true; - try { - sharp().tile(null, 8193); - } catch (err) { - isValid = false; - } - assert.strictEqual(false, isValid); - done(); - }); - - it('overlap - larger than default size', function(done) { - var isValid = true; - try { - sharp().tile(null, 257); - } catch (err) { - isValid = false; - } - assert.strictEqual(false, isValid); - done(); - }); - - it('overlap - larger than provided size', function(done) { - var isValid = true; - try { - sharp().tile(512, 513); - } catch (err) { - isValid = false; - } - assert.strictEqual(false, isValid); - done(); - }); - }); if (sharp.format.dz.output.file) { - describe('Deep Zoom output', function() { - it('Tile size - 256px default', function(done) { - var directory = fixtures.path('output.256_files'); - rimraf(directory, function() { - sharp(fixtures.inputJpg).toFile(fixtures.path('output.256.dzi'), function(err, info) { + it('Deep Zoom layout', function(done) { + var directory = fixtures.path('output.dz_files'); + rimraf(directory, function() { + sharp(fixtures.inputJpg) + .toFile(fixtures.path('output.dz.dzi'), function(err, info) { if (err) throw err; assert.strictEqual('dz', info.format); assertDeepZoomTiles(directory, 256, 13, done); }); - }); }); + }); - it('Tile size/overlap - 512/16px', function(done) { - var directory = fixtures.path('output.512_files'); - rimraf(directory, function() { - sharp(fixtures.inputJpg).tile(512, 16).toFile(fixtures.path('output.512.dzi'), function(err, info) { + it('Deep Zoom layout with custom size+overlap', function(done) { + var directory = fixtures.path('output.dz.512_files'); + rimraf(directory, function() { + sharp(fixtures.inputJpg) + .tile({ + size: 512, + overlap: 16 + }) + .toFile(fixtures.path('output.dz.512.dzi'), function(err, info) { if (err) throw err; assert.strictEqual('dz', info.format); assertDeepZoomTiles(directory, 512 + 2 * 16, 13, done); }); - }); }); - }); + + it('Zoomify layout', function(done) { + var directory = fixtures.path('output.zoomify'); + rimraf(directory, function() { + sharp(fixtures.inputJpg) + .tile({ + layout: 'zoomify' + }) + .toFile(fixtures.path('output.zoomify.dzi'), function(err, info) { + if (err) throw err; + assert.strictEqual('dz', info.format); + fs.stat(path.join(directory, 'ImageProperties.xml'), function(err, stat) { + if (err) throw err; + assert.strictEqual(true, stat.isFile()); + assert.strictEqual(true, stat.size > 0); + done(); + }); + }); + }); + }); + + it('Google layout', function(done) { + var directory = fixtures.path('output.google'); + rimraf(directory, function() { + sharp(fixtures.inputJpg) + .tile({ + layout: 'google' + }) + .toFile(fixtures.path('output.google.dzi'), function(err, info) { + if (err) throw err; + assert.strictEqual('dz', info.format); + fs.stat(path.join(directory, '0', '0', '0.jpg'), function(err, stat) { + if (err) throw err; + assert.strictEqual(true, stat.isFile()); + assert.strictEqual(true, stat.size > 0); + done(); + }); + }); + }); + }); + } });