mirror of
https://github.com/lovell/sharp.git
synced 2025-07-12 03:50:13 +02:00
Expose depth option for tile-based output (#1342)
This commit is contained in:
parent
d705cffdd6
commit
b5b95e5ae1
@ -280,6 +280,7 @@ Warning: multiple sharp instances concurrently producing tile output can expose
|
||||
- `tile.angle` **[Number][8]** tile angle of rotation, must be a multiple of 90. (optional, default `0`)
|
||||
- `tile.container` **[String][1]** tile container, with value `fs` (filesystem) or `zip` (compressed file). (optional, default `'fs'`)
|
||||
- `tile.layout` **[String][1]** filesystem layout, possible values are `dz`, `zoomify` or `google`. (optional, default `'dz'`)
|
||||
- `tile.depth` **[String][1]** pyramid depth, possible values are `onepixel`, `onetile` or `one`. (optional, default - libvips selects one based on layout)
|
||||
|
||||
### Examples
|
||||
|
||||
|
@ -423,6 +423,7 @@ function toFormat (format, options) {
|
||||
* @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.depth] depth to shrink tiles, with value 'onepixel', 'onetile' or 'one', default based on layout
|
||||
* @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}
|
||||
@ -474,6 +475,15 @@ function tile (tile) {
|
||||
throw new Error('Unsupported angle: angle must be a positive/negative multiple of 90 ' + tile.angle);
|
||||
}
|
||||
}
|
||||
|
||||
// Depth of tiles
|
||||
if (is.defined(tile.depth)) {
|
||||
if (is.string(tile.depth) && is.inArray(tile.depth, ['onepixel', 'onetile', 'one'])) {
|
||||
this.options.tileDepth = tile.depth;
|
||||
} else {
|
||||
throw new Error("Invalid tile depth '" + tile.depth + "', should be one of 'onepixel', 'onetile' or 'one'");
|
||||
}
|
||||
}
|
||||
}
|
||||
// Format
|
||||
if (is.inArray(this.options.formatOut, ['jpeg', 'png', 'webp'])) {
|
||||
|
@ -946,14 +946,22 @@ class PipelineWorker : public Nan::AsyncWorker {
|
||||
suffix = AssembleSuffixString(extname, options);
|
||||
}
|
||||
// Write DZ to file
|
||||
image.dzsave(const_cast<char*>(baton->fileOut.data()), VImage::option()
|
||||
vips::VOption *options = VImage::option()
|
||||
->set("strip", !baton->withMetadata)
|
||||
->set("tile_size", baton->tileSize)
|
||||
->set("overlap", baton->tileOverlap)
|
||||
->set("container", baton->tileContainer)
|
||||
->set("layout", baton->tileLayout)
|
||||
->set("suffix", const_cast<char*>(suffix.data()))
|
||||
->set("angle", CalculateAngleRotation(baton->tileAngle)));
|
||||
->set("angle", CalculateAngleRotation(baton->tileAngle));
|
||||
|
||||
// libvips chooses a default depth based on layout. Instead of replicating that logic here by
|
||||
// not passing anything - libvips will handle choice
|
||||
if (baton->tileDepth < VIPS_FOREIGN_DZ_DEPTH_LAST) {
|
||||
options->set("depth", baton->tileDepth);
|
||||
}
|
||||
|
||||
image.dzsave(const_cast<char*>(baton->fileOut.data()), options);
|
||||
baton->formatOut = "dz";
|
||||
} else if (baton->formatOut == "v" || (mightMatchInput && isV) ||
|
||||
(willMatchInput && inputImageType == ImageType::VIPS)) {
|
||||
@ -1321,6 +1329,17 @@ NAN_METHOD(pipeline) {
|
||||
baton->tileLayout = VIPS_FOREIGN_DZ_LAYOUT_DZ;
|
||||
}
|
||||
baton->tileFormat = AttrAsStr(options, "tileFormat");
|
||||
std::string tileDepth = AttrAsStr(options, "tileDepth");
|
||||
if (tileDepth == "onetile") {
|
||||
baton->tileDepth = VIPS_FOREIGN_DZ_DEPTH_ONETILE;
|
||||
} else if (tileDepth == "one") {
|
||||
baton->tileDepth = VIPS_FOREIGN_DZ_DEPTH_ONE;
|
||||
} else if (tileDepth == "onepixel") {
|
||||
baton->tileDepth = VIPS_FOREIGN_DZ_DEPTH_ONEPIXEL;
|
||||
} else {
|
||||
// signal that we do not want to pass any value to dzSave
|
||||
baton->tileDepth = VIPS_FOREIGN_DZ_DEPTH_LAST;
|
||||
}
|
||||
// Force random access for certain operations
|
||||
if (baton->accessMethod == VIPS_ACCESS_SEQUENTIAL && (
|
||||
baton->trimTolerance != 0 || baton->normalise ||
|
||||
|
@ -139,6 +139,7 @@ struct PipelineBaton {
|
||||
VipsForeignDzLayout tileLayout;
|
||||
std::string tileFormat;
|
||||
int tileAngle;
|
||||
VipsForeignDzDepth tileDepth;
|
||||
|
||||
PipelineBaton():
|
||||
input(nullptr),
|
||||
@ -220,7 +221,8 @@ struct PipelineBaton {
|
||||
tileOverlap(0),
|
||||
tileContainer(VIPS_FOREIGN_DZ_CONTAINER_FS),
|
||||
tileLayout(VIPS_FOREIGN_DZ_LAYOUT_DZ),
|
||||
tileAngle(0){
|
||||
tileAngle(0),
|
||||
tileDepth(VIPS_FOREIGN_DZ_DEPTH_LAST){
|
||||
background[0] = 0.0;
|
||||
background[1] = 0.0;
|
||||
background[2] = 0.0;
|
||||
|
@ -46,6 +46,51 @@ const assertDeepZoomTiles = function (directory, expectedSize, expectedLevels, d
|
||||
}, done);
|
||||
};
|
||||
|
||||
const assertZoomifyTiles = function (directory, expectedTileSize, expectedLevels, done) {
|
||||
fs.stat(path.join(directory, 'ImageProperties.xml'), function (err, stat) {
|
||||
if (err) throw err;
|
||||
assert.ok(stat.isFile());
|
||||
assert.ok(stat.size > 0);
|
||||
|
||||
let maxTileLevel = -1;
|
||||
fs.readdirSync(path.join(directory, 'TileGroup0')).forEach(function (tile) {
|
||||
// Verify tile file name
|
||||
assert.ok(/^[0-9]+-[0-9]+-[0-9]+\.jpg$/.test(tile));
|
||||
let level = parseInt(tile.split('-')[0]);
|
||||
maxTileLevel = Math.max(maxTileLevel, level);
|
||||
});
|
||||
|
||||
assert.strictEqual(maxTileLevel + 1, expectedLevels); // add one to account for zero level tile
|
||||
|
||||
done();
|
||||
});
|
||||
};
|
||||
|
||||
const assertGoogleTiles = function (directory, expectedTileSize, expectedLevels, done) {
|
||||
const levels = fs.readdirSync(directory);
|
||||
assert.strictEqual(expectedLevels, levels.length - 1); // subtract one to account for default blank tile
|
||||
|
||||
fs.stat(path.join(directory, 'blank.png'), function (err, stat) {
|
||||
if (err) throw err;
|
||||
assert.ok(stat.isFile());
|
||||
assert.ok(stat.size > 0);
|
||||
|
||||
// Basic check to confirm lowest and highest level tiles exist
|
||||
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);
|
||||
|
||||
fs.stat(path.join(directory, (expectedLevels - 1).toString(), '0', '0.jpg'), function (err, stat) {
|
||||
if (err) throw err;
|
||||
assert.strictEqual(true, stat.isFile());
|
||||
assert.strictEqual(true, stat.size > 0);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
describe('Tile', function () {
|
||||
it('Valid size values pass', function () {
|
||||
[1, 8192].forEach(function (size) {
|
||||
@ -144,6 +189,26 @@ describe('Tile', function () {
|
||||
});
|
||||
});
|
||||
|
||||
it('Valid depths pass', function () {
|
||||
['onepixel', 'onetile', 'one'].forEach(function (depth) {
|
||||
assert.doesNotThrow(function (depth) {
|
||||
sharp().tile({
|
||||
depth: depth
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('Invalid depths fail', function () {
|
||||
['depth', 1].forEach(function (depth) {
|
||||
assert.throws(function () {
|
||||
sharp().tile({
|
||||
depth: depth
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('Prevent larger overlap than default size', function () {
|
||||
assert.throws(function () {
|
||||
sharp().tile({
|
||||
@ -251,6 +316,54 @@ describe('Tile', function () {
|
||||
});
|
||||
});
|
||||
|
||||
it('Deep Zoom layout with depth of one', function (done) {
|
||||
const directory = fixtures.path('output.512_depth_one.dzi_files');
|
||||
rimraf(directory, function () {
|
||||
sharp(fixtures.inputJpg)
|
||||
.tile({
|
||||
size: 512,
|
||||
depth: 'one'
|
||||
})
|
||||
.toFile(fixtures.path('output.512_depth_one.dzi'), function (err, info) {
|
||||
if (err) throw err;
|
||||
// Verify only one depth generated
|
||||
assertDeepZoomTiles(directory, 512, 1, done);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('Deep Zoom layout with depth of onepixel', function (done) {
|
||||
const directory = fixtures.path('output.512_depth_onepixel.dzi_files');
|
||||
rimraf(directory, function () {
|
||||
sharp(fixtures.inputJpg)
|
||||
.tile({
|
||||
size: 512,
|
||||
depth: 'onepixel'
|
||||
})
|
||||
.toFile(fixtures.path('output.512_depth_onepixel.dzi'), function (err, info) {
|
||||
if (err) throw err;
|
||||
// Verify only one depth generated
|
||||
assertDeepZoomTiles(directory, 512, 13, done);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('Deep Zoom layout with depth of onetile', function (done) {
|
||||
const directory = fixtures.path('output.256_depth_onetile.dzi_files');
|
||||
rimraf(directory, function () {
|
||||
sharp(fixtures.inputJpg)
|
||||
.tile({
|
||||
size: 256,
|
||||
depth: 'onetile'
|
||||
})
|
||||
.toFile(fixtures.path('output.256_depth_onetile.dzi'), function (err, info) {
|
||||
if (err) throw err;
|
||||
// Verify only one depth generated
|
||||
assertDeepZoomTiles(directory, 256, 5, done);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('Zoomify layout', function (done) {
|
||||
const directory = fixtures.path('output.zoomify.dzi');
|
||||
rimraf(directory, function () {
|
||||
@ -275,6 +388,69 @@ describe('Tile', function () {
|
||||
});
|
||||
});
|
||||
|
||||
it('Zoomify layout with depth one', function (done) {
|
||||
const directory = fixtures.path('output.zoomify.depth_one.dzi');
|
||||
rimraf(directory, function () {
|
||||
sharp(fixtures.inputJpg)
|
||||
.tile({
|
||||
size: 256,
|
||||
layout: 'zoomify',
|
||||
depth: 'one'
|
||||
})
|
||||
.toFile(directory, 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('number', typeof info.size);
|
||||
assertZoomifyTiles(directory, 256, 1, done);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('Zoomify layout with depth onetile', function (done) {
|
||||
const directory = fixtures.path('output.zoomify.depth_onetile.dzi');
|
||||
rimraf(directory, function () {
|
||||
sharp(fixtures.inputJpg)
|
||||
.tile({
|
||||
size: 256,
|
||||
layout: 'zoomify',
|
||||
depth: 'onetile'
|
||||
})
|
||||
.toFile(directory, 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('number', typeof info.size);
|
||||
assertZoomifyTiles(directory, 256, 5, done);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('Zoomify layout with depth onepixel', function (done) {
|
||||
const directory = fixtures.path('output.zoomify.depth_onepixel.dzi');
|
||||
rimraf(directory, function () {
|
||||
sharp(fixtures.inputJpg)
|
||||
.tile({
|
||||
size: 256,
|
||||
layout: 'zoomify',
|
||||
depth: 'onepixel'
|
||||
})
|
||||
.toFile(directory, 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('number', typeof info.size);
|
||||
assertZoomifyTiles(directory, 256, 13, done);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('Google layout', function (done) {
|
||||
const directory = fixtures.path('output.google.dzi');
|
||||
rimraf(directory, function () {
|
||||
@ -410,6 +586,72 @@ describe('Tile', function () {
|
||||
});
|
||||
});
|
||||
|
||||
it('Google layout with depth one', function (done) {
|
||||
const directory = fixtures.path('output.google_depth_one.dzi');
|
||||
rimraf(directory, function () {
|
||||
sharp(fixtures.inputJpg)
|
||||
.tile({
|
||||
layout: 'google',
|
||||
depth: 'one',
|
||||
size: 256
|
||||
})
|
||||
.toFile(directory, 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('number', typeof info.size);
|
||||
|
||||
assertGoogleTiles(directory, 256, 1, done);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('Google layout with depth onepixel', function (done) {
|
||||
const directory = fixtures.path('output.google_depth_onepixel.dzi');
|
||||
rimraf(directory, function () {
|
||||
sharp(fixtures.inputJpg)
|
||||
.tile({
|
||||
layout: 'google',
|
||||
depth: 'onepixel',
|
||||
size: 256
|
||||
})
|
||||
.toFile(directory, 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('number', typeof info.size);
|
||||
|
||||
assertGoogleTiles(directory, 256, 13, done);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('Google layout with depth onetile', function (done) {
|
||||
const directory = fixtures.path('output.google_depth_onetile.dzi');
|
||||
rimraf(directory, function () {
|
||||
sharp(fixtures.inputJpg)
|
||||
.tile({
|
||||
layout: 'google',
|
||||
depth: 'onetile',
|
||||
size: 256
|
||||
})
|
||||
.toFile(directory, 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('number', typeof info.size);
|
||||
|
||||
assertGoogleTiles(directory, 256, 5, done);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('Write to ZIP container using file extension', function (done) {
|
||||
const container = fixtures.path('output.dz.container.zip');
|
||||
const extractTo = fixtures.path('output.dz.container');
|
||||
|
Loading…
x
Reference in New Issue
Block a user