diff --git a/lib/constructor.js b/lib/constructor.js index 32596649..b4364c6a 100644 --- a/lib/constructor.js +++ b/lib/constructor.js @@ -180,6 +180,8 @@ const Sharp = function (input, options) { tiffCompression: 'jpeg', tiffPredictor: 'none', tiffSquash: false, + tiffXres: 1.0, + tiffYres: 1.0, tileSize: 256, tileOverlap: 0, // Function to notify of libvips warnings diff --git a/lib/output.js b/lib/output.js index 94351f1e..881cf374 100644 --- a/lib/output.js +++ b/lib/output.js @@ -214,6 +214,8 @@ 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 * @param {Boolean} [options.predictor='none'] - compression predictor options: none, horizontal, float + * @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 * @returns {Sharp} * @throws {Error} Invalid options @@ -233,6 +235,21 @@ function tiff (options) { 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'); + } + } + 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'); + } + } // compression if (is.defined(options) && is.defined(options.compression)) { if (is.string(options.compression) && is.inArray(options.compression, ['lzw', 'deflate', 'jpeg', 'none'])) { diff --git a/src/pipeline.cc b/src/pipeline.cc index 29bce040..6dd8cbee 100644 --- a/src/pipeline.cc +++ b/src/pipeline.cc @@ -808,7 +808,9 @@ class PipelineWorker : public Nan::AsyncWorker { ->set("Q", baton->tiffQuality) ->set("squash", baton->tiffSquash) ->set("compression", baton->tiffCompression) - ->set("predictor", baton->tiffPredictor))); + ->set("predictor", baton->tiffPredictor) + ->set("xres", baton->tiffXres) + ->set("yres", baton->tiffYres))); baton->bufferOut = static_cast(area->data); baton->bufferOutLength = area->length; area->free_fn = nullptr; @@ -904,7 +906,9 @@ class PipelineWorker : public Nan::AsyncWorker { ->set("Q", baton->tiffQuality) ->set("squash", baton->tiffSquash) ->set("compression", baton->tiffCompression) - ->set("predictor", baton->tiffPredictor)); + ->set("predictor", baton->tiffPredictor) + ->set("xres", baton->tiffXres) + ->set("yres", baton->tiffYres)); baton->formatOut = "tiff"; baton->channels = std::min(baton->channels, 3); } else if (baton->formatOut == "dz" || isDz || isDzZip) { @@ -1277,6 +1281,8 @@ NAN_METHOD(pipeline) { baton->webpNearLossless = AttrTo(options, "webpNearLossless"); baton->tiffQuality = AttrTo(options, "tiffQuality"); baton->tiffSquash = AttrTo(options, "tiffSquash"); + baton->tiffXres = AttrTo(options, "tiffXres"); + baton->tiffYres = AttrTo(options, "tiffYres"); // tiff compression options baton->tiffCompression = static_cast( vips_enum_from_nick(nullptr, VIPS_TYPE_FOREIGN_TIFF_COMPRESSION, diff --git a/src/pipeline.h b/src/pipeline.h index 47e828e6..f0fdab51 100644 --- a/src/pipeline.h +++ b/src/pipeline.h @@ -109,6 +109,8 @@ struct PipelineBaton { VipsForeignTiffCompression tiffCompression; VipsForeignTiffPredictor tiffPredictor; bool tiffSquash; + double tiffXres; + double tiffYres; std::string err; bool withMetadata; int withMetadataOrientation; @@ -182,6 +184,8 @@ struct PipelineBaton { tiffCompression(VIPS_FOREIGN_TIFF_COMPRESSION_JPEG), tiffPredictor(VIPS_FOREIGN_TIFF_PREDICTOR_NONE), tiffSquash(false), + tiffXres(1.0), + tiffYres(1.0), withMetadata(false), withMetadataOrientation(-1), convKernelWidth(0), diff --git a/test/unit/io.js b/test/unit/io.js index 64a1054f..6898dc0a 100644 --- a/test/unit/io.js +++ b/test/unit/io.js @@ -933,6 +933,53 @@ describe('Input/output', function () { }); }); + it('TIFF setting xres and yres on file', function (done) { + const res = 1000.0; // inputTiff has a dpi of 300 (res*2.54) + sharp(fixtures.inputTiff) + .tiff({ + xres: (res), + yres: (res) + }) + .toFile(fixtures.outputTiff, (err, info) => { + if (err) throw err; + assert.strictEqual('tiff', info.format); + sharp(fixtures.outputTiff).metadata(function (err, metadata) { + if (err) throw err; + assert.strictEqual(metadata.density, res * 2.54); // convert to dpi + fs.unlink(fixtures.outputTiff, done); + }); + }); + }); + + it('TIFF setting xres and yres on buffer', function (done) { + const res = 1000.0; // inputTiff has a dpi of 300 (res*2.54) + sharp(fixtures.inputTiff) + .tiff({ + xres: (res), + yres: (res) + }) + .toBuffer(function (err, data, info) { + if (err) throw err; + sharp(data).metadata(function (err, metadata) { + if (err) throw err; + assert.strictEqual(metadata.density, res * 2.54); // convert to dpi + done(); + }); + }); + }); + + it('TIFF invalid xres value should throw an error', function () { + assert.throws(function () { + sharp().tiff({ xres: '1000.0' }); + }); + }); + + it('TIFF invalid yres value should throw an error', function () { + assert.throws(function () { + sharp().tiff({ yres: '1000.0' }); + }); + }); + it('TIFF lzw compression with horizontal predictor shrinks test file', function (done) { const startSize = fs.statSync(fixtures.inputTiffUncompressed).size; sharp(fixtures.inputTiffUncompressed)