Skip to content

Commit f86ae79

Browse files
BiancoAlovell
authored andcommitted
Expose angle option in tile feature (lovell#1121)
1 parent 1a4e680 commit f86ae79

4 files changed

Lines changed: 99 additions & 11 deletions

File tree

lib/output.js

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -319,6 +319,7 @@ function toFormat (format, options) {
319319
* @param {Object} [tile]
320320
* @param {Number} [tile.size=256] tile size in pixels, a value between 1 and 8192.
321321
* @param {Number} [tile.overlap=0] tile overlap in pixels, a value between 0 and 8192.
322+
* @param {Number} [tile.angle=0] tile, angle of rotation, must be a multiple of 90..
322323
* @param {String} [tile.container='fs'] tile container, with value `fs` (filesystem) or `zip` (compressed file).
323324
* @param {String} [tile.layout='dz'] filesystem layout, possible values are `dz`, `zoomify` or `google`.
324325
* @returns {Sharp}
@@ -361,13 +362,23 @@ function tile (tile) {
361362
throw new Error('Invalid tile layout ' + tile.layout);
362363
}
363364
}
365+
366+
// Angle of rotation,
367+
if (is.defined(tile.angle)) {
368+
if (is.integer(tile.angle) && !(tile.angle % 90)) {
369+
this.options.tileAngle = tile.angle;
370+
} else {
371+
throw new Error('Unsupported angle: angle must be a positive/negative multiple of 90 ' + tile.angle);
372+
}
373+
}
364374
}
365375
// Format
366376
if (is.inArray(this.options.formatOut, ['jpeg', 'png', 'webp'])) {
367377
this.options.tileFormat = this.options.formatOut;
368378
} else if (this.options.formatOut !== 'input') {
369379
throw new Error('Invalid tile format ' + this.options.formatOut);
370380
}
381+
371382
return this._updateFormatOut('dz');
372383
}
373384

src/pipeline.cc

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -917,7 +917,8 @@ class PipelineWorker : public Nan::AsyncWorker {
917917
->set("overlap", baton->tileOverlap)
918918
->set("container", baton->tileContainer)
919919
->set("layout", baton->tileLayout)
920-
->set("suffix", const_cast<char*>(suffix.data())));
920+
->set("suffix", const_cast<char*>(suffix.data()))
921+
->set("angle", CalculateAngleRotation(baton->tileAngle)));
921922
baton->formatOut = "dz";
922923
} else if (baton->formatOut == "v" || (mightMatchInput && isV) ||
923924
(willMatchInput && inputImageType == ImageType::VIPS)) {
@@ -1263,6 +1264,7 @@ NAN_METHOD(pipeline) {
12631264
baton->tileSize = AttrTo<uint32_t>(options, "tileSize");
12641265
baton->tileOverlap = AttrTo<uint32_t>(options, "tileOverlap");
12651266
std::string tileContainer = AttrAsStr(options, "tileContainer");
1267+
baton->tileAngle = AttrTo<int32_t>(options, "tileAngle");
12661268
if (tileContainer == "zip") {
12671269
baton->tileContainer = VIPS_FOREIGN_DZ_CONTAINER_ZIP;
12681270
} else {

src/pipeline.h

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,7 @@ struct PipelineBaton {
132132
VipsForeignDzContainer tileContainer;
133133
VipsForeignDzLayout tileLayout;
134134
std::string tileFormat;
135+
int tileAngle;
135136

136137
PipelineBaton():
137138
input(nullptr),
@@ -206,7 +207,8 @@ struct PipelineBaton {
206207
tileSize(256),
207208
tileOverlap(0),
208209
tileContainer(VIPS_FOREIGN_DZ_CONTAINER_FS),
209-
tileLayout(VIPS_FOREIGN_DZ_LAYOUT_DZ) {
210+
tileLayout(VIPS_FOREIGN_DZ_LAYOUT_DZ),
211+
tileAngle(0){
210212
background[0] = 0.0;
211213
background[1] = 0.0;
212214
background[2] = 0.0;

test/unit/tile.js

Lines changed: 82 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -146,13 +146,38 @@ describe('Tile', function () {
146146

147147
it('Prevent larger overlap than default size', function () {
148148
assert.throws(function () {
149-
sharp().tile({overlap: 257});
149+
sharp().tile({
150+
overlap: 257
151+
});
150152
});
151153
});
152154

153155
it('Prevent larger overlap than provided size', function () {
154156
assert.throws(function () {
155-
sharp().tile({size: 512, overlap: 513});
157+
sharp().tile({
158+
size: 512,
159+
overlap: 513
160+
});
161+
});
162+
});
163+
164+
it('Valid rotation angle values pass', function () {
165+
[90, 270, -90].forEach(function (angle) {
166+
assert.doesNotThrow(function () {
167+
sharp().tile({
168+
angle: angle
169+
});
170+
});
171+
});
172+
});
173+
174+
it('Invalid rotation angle values fail', function () {
175+
['zoinks', 1.1, -1, 27].forEach(function (angle) {
176+
assert.throws(function () {
177+
sharp().tile({
178+
angle: angle
179+
});
180+
});
156181
});
157182
});
158183

@@ -192,6 +217,40 @@ describe('Tile', function () {
192217
});
193218
});
194219

220+
it('Deep Zoom layout with custom size+angle', function (done) {
221+
const directory = fixtures.path('output.512_90.dzi_files');
222+
rimraf(directory, function () {
223+
sharp(fixtures.inputJpg)
224+
.tile({
225+
size: 512,
226+
angle: 90
227+
})
228+
.toFile(fixtures.path('output.512_90.dzi'), function (err, info) {
229+
if (err) throw err;
230+
assert.strictEqual('dz', info.format);
231+
assert.strictEqual(2725, info.width);
232+
assert.strictEqual(2225, info.height);
233+
assert.strictEqual(3, info.channels);
234+
assert.strictEqual('undefined', typeof info.size);
235+
assertDeepZoomTiles(directory, 512, 13, done);
236+
// Verifies tiles in 10th level are rotated
237+
let tile = path.join(directory, '10', '0_1.jpeg');
238+
// verify that the width and height correspond to the rotated image
239+
// expected are w=512 and h=170 for the 0_1.jpeg.
240+
// if a 0 angle is supplied to the .tile function
241+
// the expected values are w=170 and h=512 for the 1_0.jpeg
242+
sharp(tile).metadata(function (err, metadata) {
243+
if (err) {
244+
throw err;
245+
} else {
246+
assert.strictEqual(true, metadata.width === 512);
247+
assert.strictEqual(true, metadata.height === 170);
248+
}
249+
});
250+
});
251+
});
252+
});
253+
195254
it('Zoomify layout', function (done) {
196255
const directory = fixtures.path('output.zoomify.dzi');
197256
rimraf(directory, function () {
@@ -244,7 +303,9 @@ describe('Tile', function () {
244303
const directory = fixtures.path('output.jpg.google.dzi');
245304
rimraf(directory, function () {
246305
sharp(fixtures.inputJpg)
247-
.jpeg({ quality: 1 })
306+
.jpeg({
307+
quality: 1
308+
})
248309
.tile({
249310
layout: 'google'
250311
})
@@ -279,7 +340,9 @@ describe('Tile', function () {
279340
const directory = fixtures.path('output.png.google.dzi');
280341
rimraf(directory, function () {
281342
sharp(fixtures.inputJpg)
282-
.png({ compressionLevel: 1 })
343+
.png({
344+
compressionLevel: 1
345+
})
283346
.tile({
284347
layout: 'google'
285348
})
@@ -314,7 +377,9 @@ describe('Tile', function () {
314377
const directory = fixtures.path('output.webp.google.dzi');
315378
rimraf(directory, function () {
316379
sharp(fixtures.inputJpg)
317-
.webp({ quality: 1 })
380+
.webp({
381+
quality: 1
382+
})
318383
.tile({
319384
layout: 'google'
320385
})
@@ -363,8 +428,12 @@ describe('Tile', function () {
363428
assert.strictEqual(true, stat.isFile());
364429
assert.strictEqual(true, stat.size > 0);
365430
fs.createReadStream(container)
366-
.pipe(unzip.Extract({path: path.dirname(extractTo)}))
367-
.on('error', function (err) { throw err; })
431+
.pipe(unzip.Extract({
432+
path: path.dirname(extractTo)
433+
}))
434+
.on('error', function (err) {
435+
throw err;
436+
})
368437
.on('close', function () {
369438
assertDeepZoomTiles(directory, 256, 13, done);
370439
});
@@ -395,8 +464,12 @@ describe('Tile', function () {
395464
assert.strictEqual(true, stat.isFile());
396465
assert.strictEqual(true, stat.size > 0);
397466
fs.createReadStream(container)
398-
.pipe(unzip.Extract({path: path.dirname(extractTo)}))
399-
.on('error', function (err) { throw err; })
467+
.pipe(unzip.Extract({
468+
path: path.dirname(extractTo)
469+
}))
470+
.on('error', function (err) {
471+
throw err;
472+
})
400473
.on('close', function () {
401474
assertDeepZoomTiles(directory, 256, 13, done);
402475
});

0 commit comments

Comments
 (0)