From f7bed69ffb5a533881faeb02163111deb4ff219a Mon Sep 17 00:00:00 2001 From: ompal Date: Thu, 23 Dec 2021 11:38:36 +0530 Subject: [PATCH] Add resolutionUnit to metadata and as tiff option #3023 Co-authored-by: Lovell Fuller --- lib/constructor.js | 1 + lib/output.js | 9 +++++++++ package.json | 3 ++- src/metadata.cc | 6 ++++++ src/metadata.h | 1 + src/pipeline.cc | 11 +++++++++-- src/pipeline.h | 2 ++ test/unit/metadata.js | 1 + test/unit/tiff.js | 30 ++++++++++++++++++++++++++++++ 9 files changed, 61 insertions(+), 3 deletions(-) diff --git a/lib/constructor.js b/lib/constructor.js index e644f96f..5d1d63b1 100644 --- a/lib/constructor.js +++ b/lib/constructor.js @@ -262,6 +262,7 @@ const Sharp = function (input, options) { tiffTileWidth: 256, tiffXres: 1.0, tiffYres: 1.0, + tiffResolutionUnit: 'inch', heifQuality: 50, heifLossless: false, heifCompression: 'av1', diff --git a/lib/output.js b/lib/output.js index a84c9d01..28d6b5a8 100644 --- a/lib/output.js +++ b/lib/output.js @@ -704,6 +704,7 @@ function trySetAnimationOptions (source, target) { * @param {number} [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 {string} [options.resolutionUnit='inch'] - resolution unit options: inch, cm * @param {number} [options.bitdepth=8] - reduce bitdepth to 1, 2 or 4 bit * @returns {Sharp} * @throws {Error} Invalid options @@ -777,6 +778,14 @@ function tiff (options) { throw is.invalidParameterError('predictor', 'one of: none, horizontal, float', options.predictor); } } + // resolutionUnit + if (is.defined(options.resolutionUnit)) { + if (is.string(options.resolutionUnit) && is.inArray(options.resolutionUnit, ['inch', 'cm'])) { + this.options.tiffResolutionUnit = options.resolutionUnit; + } else { + throw is.invalidParameterError('resolutionUnit', 'one of: inch, cm', options.resolutionUnit); + } + } } return this._updateFormatOut('tiff', options); } diff --git a/package.json b/package.json index 3024e3a4..e2429e08 100644 --- a/package.json +++ b/package.json @@ -80,7 +80,8 @@ "Brad Parham ", "Taneli Vatanen ", "Joris Dugué ", - "Chris Banks " + "Chris Banks ", + "Ompal Singh " ], "scripts": { "install": "(node install/libvips && node install/dll-copy && prebuild-install) || (node install/can-compile && node-gyp rebuild && node install/dll-copy)", diff --git a/src/metadata.cc b/src/metadata.cc index 9143b594..1dd01047 100644 --- a/src/metadata.cc +++ b/src/metadata.cc @@ -77,6 +77,9 @@ class MetadataWorker : public Napi::AsyncWorker { if (image.get_typeof("heif-compression") == VIPS_TYPE_REF_STRING) { baton->compression = image.get_string("heif-compression"); } + if (image.get_typeof(VIPS_META_RESOLUTION_UNIT) == VIPS_TYPE_REF_STRING) { + baton->resolutionUnit = image.get_string(VIPS_META_RESOLUTION_UNIT); + } if (image.get_typeof("openslide.level-count") == VIPS_TYPE_REF_STRING) { int const levels = std::stoi(image.get_string("openslide.level-count")); for (int l = 0; l < levels; l++) { @@ -198,6 +201,9 @@ class MetadataWorker : public Napi::AsyncWorker { if (!baton->compression.empty()) { info.Set("compression", baton->compression); } + if (!baton->resolutionUnit.empty()) { + info.Set("resolutionUnit", baton->resolutionUnit == "in" ? "inch" : baton->resolutionUnit); + } if (!baton->levels.empty()) { int i = 0; Napi::Array levels = Napi::Array::New(env, static_cast(baton->levels.size())); diff --git a/src/metadata.h b/src/metadata.h index 19816a65..593421d2 100644 --- a/src/metadata.h +++ b/src/metadata.h @@ -40,6 +40,7 @@ struct MetadataBaton { std::vector delay; int pagePrimary; std::string compression; + std::string resolutionUnit; std::vector> levels; int subifds; std::vector background; diff --git a/src/pipeline.cc b/src/pipeline.cc index cc38cef7..203e6011 100644 --- a/src/pipeline.cc +++ b/src/pipeline.cc @@ -913,7 +913,8 @@ class PipelineWorker : public Napi::AsyncWorker { ->set("tile_height", baton->tiffTileHeight) ->set("tile_width", baton->tiffTileWidth) ->set("xres", baton->tiffXres) - ->set("yres", baton->tiffYres))); + ->set("yres", baton->tiffYres) + ->set("resunit", baton->tiffResolutionUnit))); baton->bufferOut = static_cast(area->data); baton->bufferOutLength = area->length; area->free_fn = nullptr; @@ -1071,7 +1072,8 @@ class PipelineWorker : public Napi::AsyncWorker { ->set("tile_height", baton->tiffTileHeight) ->set("tile_width", baton->tiffTileWidth) ->set("xres", baton->tiffXres) - ->set("yres", baton->tiffYres)); + ->set("yres", baton->tiffYres) + ->set("resunit", baton->tiffResolutionUnit)); baton->formatOut = "tiff"; } else if (baton->formatOut == "heif" || (mightMatchInput && isHeif) || (willMatchInput && inputImageType == sharp::ImageType::HEIF)) { @@ -1542,6 +1544,10 @@ Napi::Value pipeline(const Napi::CallbackInfo& info) { baton->tiffPredictor = static_cast( vips_enum_from_nick(nullptr, VIPS_TYPE_FOREIGN_TIFF_PREDICTOR, sharp::AttrAsStr(options, "tiffPredictor").data())); + baton->tiffResolutionUnit = static_cast( + vips_enum_from_nick(nullptr, VIPS_TYPE_FOREIGN_TIFF_RESUNIT, + sharp::AttrAsStr(options, "tiffResolutionUnit").data())); + baton->heifQuality = sharp::AttrAsUint32(options, "heifQuality"); baton->heifLossless = sharp::AttrAsBool(options, "heifLossless"); baton->heifCompression = static_cast( @@ -1550,6 +1556,7 @@ Napi::Value pipeline(const Napi::CallbackInfo& info) { baton->heifEffort = sharp::AttrAsUint32(options, "heifEffort"); baton->heifChromaSubsampling = sharp::AttrAsStr(options, "heifChromaSubsampling"); + // Raw output baton->rawDepth = static_cast( vips_enum_from_nick(nullptr, VIPS_TYPE_BAND_FORMAT, diff --git a/src/pipeline.h b/src/pipeline.h index f9d95b18..0b27f5a9 100644 --- a/src/pipeline.h +++ b/src/pipeline.h @@ -167,6 +167,7 @@ struct PipelineBaton { int tiffTileWidth; double tiffXres; double tiffYres; + VipsForeignTiffResunit tiffResolutionUnit; int heifQuality; VipsForeignHeifCompression heifCompression; int heifEffort; @@ -305,6 +306,7 @@ struct PipelineBaton { tiffTileWidth(256), tiffXres(1.0), tiffYres(1.0), + tiffResolutionUnit(VIPS_FOREIGN_TIFF_RESUNIT_INCH), heifQuality(50), heifCompression(VIPS_FOREIGN_HEIF_COMPRESSION_AV1), heifEffort(4), diff --git a/test/unit/metadata.js b/test/unit/metadata.js index 76a842a9..a8bde5d4 100644 --- a/test/unit/metadata.js +++ b/test/unit/metadata.js @@ -99,6 +99,7 @@ describe('Image metadata', function () { assert.strictEqual(1, metadata.orientation); assert.strictEqual('undefined', typeof metadata.exif); assert.strictEqual('undefined', typeof metadata.icc); + assert.strictEqual('inch', metadata.resolutionUnit); done(); }); }); diff --git a/test/unit/tiff.js b/test/unit/tiff.js index e143927d..0c4aed53 100644 --- a/test/unit/tiff.js +++ b/test/unit/tiff.js @@ -288,6 +288,30 @@ describe('TIFF', function () { }); }); + it('TIFF resolutionUnit of inch (default)', async () => { + const data = await sharp({ create: { width: 8, height: 8, channels: 3, background: 'red' } }) + .tiff() + .toBuffer(); + const { resolutionUnit } = await sharp(data).metadata(); + assert.strictEqual(resolutionUnit, 'inch'); + }); + + it('TIFF resolutionUnit of inch', async () => { + const data = await sharp({ create: { width: 8, height: 8, channels: 3, background: 'red' } }) + .tiff({ resolutionUnit: 'inch' }) + .toBuffer(); + const { resolutionUnit } = await sharp(data).metadata(); + assert.strictEqual(resolutionUnit, 'inch'); + }); + + it('TIFF resolutionUnit of cm', async () => { + const data = await sharp({ create: { width: 8, height: 8, channels: 3, background: 'red' } }) + .tiff({ resolutionUnit: 'cm' }) + .toBuffer(); + const { resolutionUnit } = await sharp(data).metadata(); + assert.strictEqual(resolutionUnit, 'cm'); + }); + it('TIFF deflate compression with horizontal predictor shrinks test file', function (done) { const startSize = fs.statSync(fixtures.inputTiffUncompressed).size; sharp(fixtures.inputTiffUncompressed) @@ -383,6 +407,12 @@ describe('TIFF', function () { }); }); + it('TIFF invalid resolutionUnit option throws', function () { + assert.throws(function () { + sharp().tiff({ resolutionUnit: 'none' }); + }); + }); + it('TIFF horizontal predictor does not throw error', function () { assert.doesNotThrow(function () { sharp().tiff({ predictor: 'horizontal' });