Add support for Zoomify and Google tile layouts

Breaks existing tile API
This commit is contained in:
Lovell Fuller 2016-03-03 20:29:23 +00:00
parent f950294f70
commit 38ddb3b866
6 changed files with 176 additions and 144 deletions

View File

@ -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. 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. * `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. * `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 ```javascript
sharp('input.tiff').tile(256).toFile('output.dzi', function(err, info) { sharp('input.tiff')
// The output.dzi file is the XML format Deep Zoom definition .tile({
// The output_files directory contains 256x256 pixel tiles grouped by zoom level 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() #### withoutChromaSubsampling()

View File

@ -6,6 +6,10 @@
[#128](https://github.com/lovell/sharp/issues/128) [#128](https://github.com/lovell/sharp/issues/128)
[@blowsie](https://github.com/blowsie) [@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. * Improvements to overlayWith: differing sizes/formats, gravity, buffer input.
[#239](https://github.com/lovell/sharp/issues/239) [#239](https://github.com/lovell/sharp/issues/239)
[@chrisriley](https://github.com/chrisriley) [@chrisriley](https://github.com/chrisriley)

View File

@ -166,6 +166,9 @@ var isInteger = function(val) {
var inRange = function(val, min, max) { var inRange = function(val, min, max) {
return val >= min && val <= max; return val >= min && val <= max;
}; };
var contains = function(val, list) {
return list.indexOf(val) !== -1;
};
/* /*
Set input-related options 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) { Sharp.prototype.tile = function(tile) {
if (isObject(tile)) {
// Size of square tiles, in pixels // Size of square tiles, in pixels
if (typeof size !== 'undefined' && size !== null) { if (isDefined(tile.size)) {
if (!Number.isNaN(size) && size % 1 === 0 && size >= 1 && size <= 8192) { if (isInteger(tile.size) && inRange(tile.size, 1, 8192)) {
this.options.tileSize = size; this.options.tileSize = tile.size;
} else { } else {
throw new Error('Invalid tile size (1 to 8192) ' + size); throw new Error('Invalid tile size (1 to 8192) ' + tile.size);
} }
} }
// Overlap of tiles, in pixels // Overlap of tiles, in pixels
if (typeof overlap !== 'undefined' && overlap !== null) { if (isDefined(tile.overlap)) {
if (!Number.isNaN(overlap) && overlap % 1 === 0 && overlap >= 0 && overlap <= 8192) { if (isInteger(tile.overlap) && inRange(tile.overlap, 0, 8192)) {
if (overlap > this.options.tileSize) { if (tile.overlap > this.options.tileSize) {
throw new Error('Tile overlap ' + overlap + ' cannot be larger than tile size ' + this.options.tileSize); throw new Error('Tile overlap ' + tile.overlap + ' cannot be larger than tile size ' + this.options.tileSize);
} }
this.options.tileOverlap = overlap; this.options.tileOverlap = tile.overlap;
} else { } else {
throw new Error('Invalid tile overlap (0 to 8192) ' + overlap); 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);
}
} }
} }
return this; return this;

View File

@ -766,6 +766,7 @@ class PipelineWorker : public AsyncWorker {
->set("strip", !baton->withMetadata) ->set("strip", !baton->withMetadata)
->set("tile_size", baton->tileSize) ->set("tile_size", baton->tileSize)
->set("overlap", baton->tileOverlap) ->set("overlap", baton->tileOverlap)
->set("layout", baton->tileLayout)
); );
baton->formatOut = "dz"; baton->formatOut = "dz";
} else { } else {
@ -1030,8 +1031,18 @@ NAN_METHOD(pipeline) {
// Output // Output
baton->formatOut = attrAsStr(options, "formatOut"); baton->formatOut = attrAsStr(options, "formatOut");
baton->fileOut = attrAsStr(options, "fileOut"); baton->fileOut = attrAsStr(options, "fileOut");
// Tile output
baton->tileSize = attrAs<int32_t>(options, "tileSize"); baton->tileSize = attrAs<int32_t>(options, "tileSize");
baton->tileOverlap = attrAs<int32_t>(options, "tileOverlap"); baton->tileOverlap = attrAs<int32_t>(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 // Function to notify of queue length changes
Callback *queueListener = new Callback( Callback *queueListener = new Callback(
Get(options, New("queueListener").ToLocalChecked()).ToLocalChecked().As<Function>() Get(options, New("queueListener").ToLocalChecked()).ToLocalChecked().As<Function>()

View File

@ -1,6 +1,8 @@
#ifndef SRC_PIPELINE_H_ #ifndef SRC_PIPELINE_H_
#define SRC_PIPELINE_H_ #define SRC_PIPELINE_H_
#include <vips/vips8>
#include "nan.h" #include "nan.h"
NAN_METHOD(pipeline); NAN_METHOD(pipeline);
@ -79,6 +81,7 @@ struct PipelineBaton {
int withMetadataOrientation; int withMetadataOrientation;
int tileSize; int tileSize;
int tileOverlap; int tileOverlap;
VipsForeignDzLayout tileLayout;
PipelineBaton(): PipelineBaton():
bufferInLength(0), bufferInLength(0),
@ -126,7 +129,8 @@ struct PipelineBaton {
withMetadata(false), withMetadata(false),
withMetadataOrientation(-1), withMetadataOrientation(-1),
tileSize(256), tileSize(256),
tileOverlap(0) { tileOverlap(0),
tileLayout(VIPS_FOREIGN_DZ_LAYOUT_DZ) {
background[0] = 0.0; background[0] = 0.0;
background[1] = 0.0; background[1] = 0.0;
background[2] = 0.0; background[2] = 0.0;

View File

@ -47,137 +47,86 @@ var assertDeepZoomTiles = function(directory, expectedSize, expectedLevels, done
describe('Tile', function() { describe('Tile', function() {
describe('Invalid tile values', function() { it('Valid size values pass', function() {
it('size - NaN', function(done) { [1, 8192].forEach(function(size) {
var isValid = true; assert.doesNotThrow(function() {
try { sharp().tile({
sharp().tile('zoinks'); size: size
} catch (err) { });
isValid = false; });
} });
assert.strictEqual(false, isValid);
done();
}); });
it('size - float', function(done) { it('Invalid size values fail', function() {
var isValid = true; ['zoinks', 1.1, -1, 0, 8193].forEach(function(size) {
try { assert.throws(function() {
sharp().tile(1.1); sharp().tile({
} catch (err) { size: size
isValid = false; });
} });
assert.strictEqual(false, isValid); });
done();
}); });
it('size - negative', function(done) { it('Valid overlap values pass', function() {
var isValid = true; [0, 8192].forEach(function(overlap) {
try { assert.doesNotThrow(function() {
sharp().tile(-1); sharp().tile({
} catch (err) { size: 8192,
isValid = false; overlap: overlap
} });
assert.strictEqual(false, isValid); });
done(); });
}); });
it('size - zero', function(done) { it('Invalid overlap values fail', function() {
var isValid = true; ['zoinks', 1.1, -1, 8193].forEach(function(overlap) {
try { assert.throws(function() {
sharp().tile(0); sharp().tile({
} catch (err) { overlap: overlap
isValid = false; });
} });
assert.strictEqual(false, isValid); });
done();
}); });
it('size - too large', function(done) { it('Valid layout values pass', function() {
var isValid = true; ['dz', 'google', 'zoomify'].forEach(function(layout) {
try { assert.doesNotThrow(function() {
sharp().tile(8193); sharp().tile({
} catch (err) { layout: layout
isValid = false; });
} });
assert.strictEqual(false, isValid); });
done();
}); });
it('overlap - NaN', function(done) { it('Invalid layout values fail', function() {
var isValid = true; ['zoinks', 1].forEach(function(layout) {
try { assert.throws(function() {
sharp().tile(null, 'zoinks'); sharp().tile({
} catch (err) { layout: layout
isValid = false; });
} });
assert.strictEqual(false, isValid); });
done();
}); });
it('overlap - float', function(done) { it('Prevent larger overlap than default size', function() {
var isValid = true; assert.throws(function() {
try { sharp().tile({overlap: 257});
sharp().tile(null, 1.1); });
} catch (err) {
isValid = false;
}
assert.strictEqual(false, isValid);
done();
}); });
it('overlap - negative', function(done) { it('Prevent larger overlap than provided size', function() {
var isValid = true; assert.throws(function() {
try { sharp().tile({size: 512, overlap: 513});
sharp().tile(null, -1);
} catch (err) {
isValid = false;
}
assert.strictEqual(false, isValid);
done();
}); });
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) { if (sharp.format.dz.output.file) {
describe('Deep Zoom output', function() {
it('Tile size - 256px default', function(done) { it('Deep Zoom layout', function(done) {
var directory = fixtures.path('output.256_files'); var directory = fixtures.path('output.dz_files');
rimraf(directory, function() { rimraf(directory, function() {
sharp(fixtures.inputJpg).toFile(fixtures.path('output.256.dzi'), function(err, info) { sharp(fixtures.inputJpg)
.toFile(fixtures.path('output.dz.dzi'), function(err, info) {
if (err) throw err; if (err) throw err;
assert.strictEqual('dz', info.format); assert.strictEqual('dz', info.format);
assertDeepZoomTiles(directory, 256, 13, done); assertDeepZoomTiles(directory, 256, 13, done);
@ -185,10 +134,15 @@ describe('Tile', function() {
}); });
}); });
it('Tile size/overlap - 512/16px', function(done) { it('Deep Zoom layout with custom size+overlap', function(done) {
var directory = fixtures.path('output.512_files'); var directory = fixtures.path('output.dz.512_files');
rimraf(directory, function() { rimraf(directory, function() {
sharp(fixtures.inputJpg).tile(512, 16).toFile(fixtures.path('output.512.dzi'), function(err, info) { sharp(fixtures.inputJpg)
.tile({
size: 512,
overlap: 16
})
.toFile(fixtures.path('output.dz.512.dzi'), function(err, info) {
if (err) throw err; if (err) throw err;
assert.strictEqual('dz', info.format); assert.strictEqual('dz', info.format);
assertDeepZoomTiles(directory, 512 + 2 * 16, 13, done); assertDeepZoomTiles(directory, 512 + 2 * 16, 13, done);
@ -196,7 +150,46 @@ describe('Tile', function() {
}); });
}); });
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();
});
});
});
});
} }
}); });