diff --git a/lib/constructor.js b/lib/constructor.js index c78378db..e848fe7a 100644 --- a/lib/constructor.js +++ b/lib/constructor.js @@ -177,7 +177,11 @@ const Sharp = function (input, options) { tiffQuality: 80, tiffCompression: 'jpeg', tiffPredictor: 'horizontal', + tiffPyramid: false, tiffSquash: false, + tiffTile: false, + tiffTileHeight: 256, + tiffTileWidth: 256, tiffXres: 1.0, tiffYres: 1.0, tileSize: 256, diff --git a/lib/output.js b/lib/output.js index 9556c90a..58849854 100644 --- a/lib/output.js +++ b/lib/output.js @@ -304,6 +304,10 @@ function webp (options) { * @param {Boolean} [options.force=true] - force TIFF output, otherwise attempt to use input format * @param {Boolean} [options.compression='jpeg'] - compression options: lzw, deflate, jpeg, ccittfax4 * @param {Boolean} [options.predictor='horizontal'] - compression predictor options: none, horizontal, float + * @param {Boolean} [options.pyramid=false] - write an image pyramid + * @param {Boolean} [options.tile=false] - write a tiled tiff + * @param {Boolean} [options.tileWidth=256] - horizontal tile size + * @param {Boolean} [options.tileHeight=256] - vertical tile size * @param {Number} [options.xres=1.0] - horizontal resolution in pixels/mm * @param {Number} [options.yres=1.0] - vertical resolution in pixels/mm * @param {Boolean} [options.squash=false] - squash 8-bit images down to 1 bit @@ -311,51 +315,83 @@ function webp (options) { * @throws {Error} Invalid options */ function tiff (options) { - if (is.object(options) && is.defined(options.quality)) { - if (is.integer(options.quality) && is.inRange(options.quality, 1, 100)) { - this.options.tiffQuality = options.quality; - } else { - throw new Error('Invalid quality (integer, 1-100) ' + options.quality); + if (is.object(options)) { + if (is.defined(options.quality)) { + if (is.integer(options.quality) && is.inRange(options.quality, 1, 100)) { + this.options.tiffQuality = options.quality; + } else { + throw new Error('Invalid quality (integer, 1-100) ' + options.quality); + } } - } - if (is.object(options) && is.defined(options.squash)) { - if (is.bool(options.squash)) { - this.options.tiffSquash = options.squash; - } else { - throw new Error('Invalid Value for squash ' + options.squash + ' Only Boolean Values allowed for options.squash.'); + if (is.defined(options.squash)) { + if (is.bool(options.squash)) { + this.options.tiffSquash = options.squash; + } else { + throw new Error('Invalid Value for squash ' + options.squash + ' Only Boolean Values allowed for options.squash.'); + } } - } - // resolution - if (is.object(options) && is.defined(options.xres)) { - if (is.number(options.xres)) { - this.options.tiffXres = options.xres; - } else { - throw new Error('Invalid Value for xres ' + options.xres + ' Only numeric values allowed for options.xres'); + // tiling + if (is.defined(options.tile)) { + if (is.bool(options.tile)) { + this.options.tiffTile = options.tile; + } else { + throw new Error('Invalid Value for tile ' + options.tile + ' Only Boolean values allowed for options.tile'); + } } - } - if (is.object(options) && is.defined(options.yres)) { - if (is.number(options.yres)) { - this.options.tiffYres = options.yres; - } else { - throw new Error('Invalid Value for yres ' + options.yres + ' Only numeric values allowed for options.yres'); + if (is.defined(options.tileWidth)) { + if (is.number(options.tileWidth) && options.tileWidth > 0) { + this.options.tiffTileWidth = options.tileWidth; + } else { + throw new Error('Invalid Value for tileWidth ' + options.tileWidth + ' Only positive numeric values allowed for options.tileWidth'); + } } - } - // compression - if (is.defined(options) && is.defined(options.compression)) { - if (is.string(options.compression) && is.inArray(options.compression, ['lzw', 'deflate', 'jpeg', 'ccittfax4', 'none'])) { - this.options.tiffCompression = options.compression; - } else { - const message = `Invalid compression option "${options.compression}". Should be one of: lzw, deflate, jpeg, ccittfax4, none`; - throw new Error(message); + if (is.defined(options.tileHeight)) { + if (is.number(options.tileHeight) && options.tileHeight > 0) { + this.options.tiffTileHeight = options.tileHeight; + } else { + throw new Error('Invalid Value for tileHeight ' + options.tileHeight + ' Only positive numeric values allowed for options.tileHeight'); + } } - } - // predictor - if (is.defined(options) && is.defined(options.predictor)) { - if (is.string(options.predictor) && is.inArray(options.predictor, ['none', 'horizontal', 'float'])) { - this.options.tiffPredictor = options.predictor; - } else { - const message = `Invalid predictor option "${options.predictor}". Should be one of: none, horizontal, float`; - throw new Error(message); + // pyramid + if (is.defined(options.pyramid)) { + if (is.bool(options.pyramid)) { + this.options.tiffPyramid = options.pyramid; + } else { + throw new Error('Invalid Value for pyramid ' + options.pyramid + ' Only Boolean values allowed for options.pyramid'); + } + } + // resolution + if (is.defined(options.xres)) { + if (is.number(options.xres)) { + this.options.tiffXres = options.xres; + } else { + throw new Error('Invalid Value for xres ' + options.xres + ' Only numeric values allowed for options.xres'); + } + } + if (is.defined(options.yres)) { + if (is.number(options.yres)) { + this.options.tiffYres = options.yres; + } else { + throw new Error('Invalid Value for yres ' + options.yres + ' Only numeric values allowed for options.yres'); + } + } + // compression + if (is.defined(options.compression)) { + if (is.string(options.compression) && is.inArray(options.compression, ['lzw', 'deflate', 'jpeg', 'ccittfax4', 'none'])) { + this.options.tiffCompression = options.compression; + } else { + const message = `Invalid compression option "${options.compression}". Should be one of: lzw, deflate, jpeg, ccittfax4, none`; + throw new Error(message); + } + } + // predictor + if (is.defined(options.predictor)) { + if (is.string(options.predictor) && is.inArray(options.predictor, ['none', 'horizontal', 'float'])) { + this.options.tiffPredictor = options.predictor; + } else { + const message = `Invalid predictor option "${options.predictor}". Should be one of: none, horizontal, float`; + throw new Error(message); + } } } return this._updateFormatOut('tiff', options); diff --git a/package.json b/package.json index 9d81d884..149fc380 100644 --- a/package.json +++ b/package.json @@ -58,7 +58,8 @@ "Freezy ", "Daiz ", "Julian Aubourg ", - "Keith Belovay " + "Keith Belovay ", + "Michael B. Klein " ], "scripts": { "install": "(node install/libvips && node install/dll-copy && prebuild-install) || (node-gyp rebuild && node install/dll-copy)", diff --git a/src/pipeline.cc b/src/pipeline.cc index 6258b11a..c3008126 100644 --- a/src/pipeline.cc +++ b/src/pipeline.cc @@ -763,6 +763,10 @@ class PipelineWorker : public Nan::AsyncWorker { ->set("squash", baton->tiffSquash) ->set("compression", baton->tiffCompression) ->set("predictor", baton->tiffPredictor) + ->set("pyramid", baton->tiffPyramid) + ->set("tile", baton->tiffTile) + ->set("tile_height", baton->tiffTileHeight) + ->set("tile_width", baton->tiffTileWidth) ->set("xres", baton->tiffXres) ->set("yres", baton->tiffYres))); baton->bufferOut = static_cast(area->data); @@ -862,6 +866,10 @@ class PipelineWorker : public Nan::AsyncWorker { ->set("squash", baton->tiffSquash) ->set("compression", baton->tiffCompression) ->set("predictor", baton->tiffPredictor) + ->set("pyramid", baton->tiffPyramid) + ->set("tile", baton->tiffTile) + ->set("tile_height", baton->tiffTileHeight) + ->set("tile_width", baton->tiffTileWidth) ->set("xres", baton->tiffXres) ->set("yres", baton->tiffYres)); baton->formatOut = "tiff"; @@ -1272,7 +1280,11 @@ NAN_METHOD(pipeline) { baton->webpLossless = AttrTo(options, "webpLossless"); baton->webpNearLossless = AttrTo(options, "webpNearLossless"); baton->tiffQuality = AttrTo(options, "tiffQuality"); + baton->tiffPyramid = AttrTo(options, "tiffPyramid"); baton->tiffSquash = AttrTo(options, "tiffSquash"); + baton->tiffTile = AttrTo(options, "tiffTile"); + baton->tiffTileWidth = AttrTo(options, "tiffTileWidth"); + baton->tiffTileHeight = AttrTo(options, "tiffTileHeight"); baton->tiffXres = AttrTo(options, "tiffXres"); baton->tiffYres = AttrTo(options, "tiffYres"); // tiff compression options diff --git a/src/pipeline.h b/src/pipeline.h index 5e020a57..b74ee304 100644 --- a/src/pipeline.h +++ b/src/pipeline.h @@ -122,7 +122,11 @@ struct PipelineBaton { int tiffQuality; VipsForeignTiffCompression tiffCompression; VipsForeignTiffPredictor tiffPredictor; + bool tiffPyramid; bool tiffSquash; + bool tiffTile; + int tiffTileHeight; + int tiffTileWidth; double tiffXres; double tiffYres; std::string err; @@ -215,7 +219,11 @@ struct PipelineBaton { tiffQuality(80), tiffCompression(VIPS_FOREIGN_TIFF_COMPRESSION_JPEG), tiffPredictor(VIPS_FOREIGN_TIFF_PREDICTOR_HORIZONTAL), + tiffPyramid(false), tiffSquash(false), + tiffTile(false), + tiffTileHeight(256), + tiffTileWidth(256), tiffXres(1.0), tiffYres(1.0), withMetadata(false), diff --git a/test/unit/io.js b/test/unit/io.js index d2bd0980..5392dbf7 100644 --- a/test/unit/io.js +++ b/test/unit/io.js @@ -1285,6 +1285,84 @@ describe('Input/output', function () { }); }); + it('TIFF tiled pyramid image without compression enlarges test file', function (done) { + const startSize = fs.statSync(fixtures.inputTiffUncompressed).size; + sharp(fixtures.inputTiffUncompressed) + .tiff({ + compression: 'none', + pyramid: true, + tile: true, + tileHeight: 256, + tileWidth: 256 + }) + .toFile(fixtures.outputTiff, (err, info) => { + if (err) throw err; + assert.strictEqual('tiff', info.format); + assert(info.size > startSize); + rimraf(fixtures.outputTiff, done); + }); + }); + + it('TIFF pyramid true value does not throw error', function () { + assert.doesNotThrow(function () { + sharp().tiff({ pyramid: true }); + }); + }); + + it('Invalid TIFF pyramid value throws error', function () { + assert.throws(function () { + sharp().tiff({ pyramid: 'true' }); + }); + }); + + it('Invalid TIFF tile value throws error', function () { + assert.throws(function () { + sharp().tiff({ tile: 'true' }); + }); + }); + + it('TIFF tile true value does not throw error', function () { + assert.doesNotThrow(function () { + sharp().tiff({ tile: true }); + }); + }); + + it('Valid TIFF tileHeight value does not throw error', function () { + assert.doesNotThrow(function () { + sharp().tiff({ tileHeight: 512 }); + }); + }); + + it('Valid TIFF tileWidth value does not throw error', function () { + assert.doesNotThrow(function () { + sharp().tiff({ tileWidth: 512 }); + }); + }); + + it('Invalid TIFF tileHeight value throws error', function () { + assert.throws(function () { + sharp().tiff({ tileHeight: '256' }); + }); + }); + + it('Invalid TIFF tileWidth value throws error', function () { + assert.throws(function () { + sharp().tiff({ tileWidth: '256' }); + }); + }); + + it('Invalid TIFF tileHeight value throws error', function () { + assert.throws(function () { + sharp().tiff({ tileHeight: 0 }); + }); + }); + + it('Invalid TIFF tileWidth value throws error', function () { + assert.throws(function () { + sharp().tiff({ tileWidth: 0 }); + }); + }); + it('Input and output formats match when not forcing', function (done) { sharp(fixtures.inputJpg) .resize(320, 240)