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.
#### 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()

View File

@ -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)

View File

@ -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;

View File

@ -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<int32_t>(options, "tileSize");
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
Callback *queueListener = new Callback(
Get(options, New("queueListener").ToLocalChecked()).ToLocalChecked().As<Function>()

View File

@ -1,6 +1,8 @@
#ifndef SRC_PIPELINE_H_
#define SRC_PIPELINE_H_
#include <vips/vips8>
#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;

View File

@ -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();
});
});
});
});
}
});