From 43a085d1ae126016bd7c9be74881983554214394 Mon Sep 17 00:00:00 2001 From: Lovell Fuller Date: Fri, 2 Apr 2021 08:04:21 +0100 Subject: [PATCH] Add support for OME-TIFF subIFDs #2557 --- docs/api-constructor.md | 1 + docs/api-input.md | 1 + docs/changelog.md | 5 ++++- lib/constructor.js | 1 + lib/input.js | 15 ++++++++++++--- src/common.cc | 10 ++++++++++ src/common.h | 2 ++ src/metadata.cc | 6 ++++++ src/metadata.h | 2 ++ test/unit/io.js | 13 +++++++++++++ 10 files changed, 52 insertions(+), 4 deletions(-) diff --git a/docs/api-constructor.md b/docs/api-constructor.md index 0fb93a54..55d88c44 100644 --- a/docs/api-constructor.md +++ b/docs/api-constructor.md @@ -28,6 +28,7 @@ Implements the [stream.Duplex][1] class. - `options.density` **[number][8]** number representing the DPI for vector images in the range 1 to 100000. (optional, default `72`) - `options.pages` **[number][8]** number of pages to extract for multi-page input (GIF, WebP, AVIF, TIFF, PDF), use -1 for all pages. (optional, default `1`) - `options.page` **[number][8]** page number to start extracting from for multi-page input (GIF, WebP, AVIF, TIFF, PDF), zero based. (optional, default `0`) + - `options.subifd` **[number][8]** subIFD (Sub Image File Directory) to extract for OME-TIFF, defaults to main image. (optional, default `-1`) - `options.level` **[number][8]** level to extract from a multi-level input (OpenSlide), zero based. (optional, default `0`) - `options.animated` **[boolean][7]** Set to `true` to read all frames/pages of an animated image (equivalent of setting `pages` to `-1`). (optional, default `false`) - `options.raw` **[Object][6]?** describes raw pixel input image data. See `raw()` for pixel ordering. diff --git a/docs/api-input.md b/docs/api-input.md index 0b2509d1..1e6092f0 100644 --- a/docs/api-input.md +++ b/docs/api-input.md @@ -21,6 +21,7 @@ A `Promise` is returned when `callback` is not provided. - `delay`: Delay in ms between each page in an animated image, provided as an array of integers. - `pagePrimary`: Number of the primary page in a HEIF image - `levels`: Details of each level in a multi-level image provided as an array of objects, requires libvips compiled with support for OpenSlide +- `subifds`: Number of Sub Image File Directories in an OME-TIFF image - `hasProfile`: Boolean indicating the presence of an embedded ICC profile - `hasAlpha`: Boolean indicating the presence of an alpha transparency channel - `orientation`: Number value of the EXIF Orientation header, if present diff --git a/docs/changelog.md b/docs/changelog.md index 0cb5328f..d6c7f57d 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -8,8 +8,11 @@ Requires libvips v8.10.6 * Ensure all installation errors are logged with a more obvious prefix. +* Add support for OME-TIFF Sub Image File Directories (subIFD). + [#2557](https://github.com/lovell/sharp/issues/2557) + * Allow `ensureAlpha` to set the alpha transparency level. - [#2634](https://github.com/lovell/sharp/issues/2634) + [#2634](https://github.com/lovell/sharp/issues/2634) ### v0.28.0 - 29th March 2021 diff --git a/lib/constructor.js b/lib/constructor.js index 7edd5515..34679d6d 100644 --- a/lib/constructor.js +++ b/lib/constructor.js @@ -132,6 +132,7 @@ const debuglog = util.debuglog('sharp'); * @param {number} [options.density=72] - number representing the DPI for vector images in the range 1 to 100000. * @param {number} [options.pages=1] - number of pages to extract for multi-page input (GIF, WebP, AVIF, TIFF, PDF), use -1 for all pages. * @param {number} [options.page=0] - page number to start extracting from for multi-page input (GIF, WebP, AVIF, TIFF, PDF), zero based. + * @param {number} [options.subifd=-1] - subIFD (Sub Image File Directory) to extract for OME-TIFF, defaults to main image. * @param {number} [options.level=0] - level to extract from a multi-level input (OpenSlide), zero based. * @param {boolean} [options.animated=false] - Set to `true` to read all frames/pages of an animated image (equivalent of setting `pages` to `-1`). * @param {Object} [options.raw] - describes raw pixel input image data. See `raw()` for pixel ordering. diff --git a/lib/input.js b/lib/input.js index a81036d6..21cff583 100644 --- a/lib/input.js +++ b/lib/input.js @@ -9,9 +9,9 @@ const sharp = require('../build/Release/sharp.node'); * @private */ function _inputOptionsFromObject (obj) { - const { raw, density, limitInputPixels, sequentialRead, failOnError, animated, page, pages } = obj; - return [raw, density, limitInputPixels, sequentialRead, failOnError, animated, page, pages].some(is.defined) - ? { raw, density, limitInputPixels, sequentialRead, failOnError, animated, page, pages } + const { raw, density, limitInputPixels, sequentialRead, failOnError, animated, page, pages, subifd } = obj; + return [raw, density, limitInputPixels, sequentialRead, failOnError, animated, page, pages, subifd].some(is.defined) + ? { raw, density, limitInputPixels, sequentialRead, failOnError, animated, page, pages, subifd } : undefined; } @@ -131,6 +131,14 @@ function _createInputDescriptor (input, inputOptions, containerOptions) { throw is.invalidParameterError('level', 'integer between 0 and 256', inputOptions.level); } } + // Sub Image File Directory (TIFF) + if (is.defined(inputOptions.subifd)) { + if (is.integer(inputOptions.subifd) && is.inRange(inputOptions.subifd, -1, 100000)) { + inputDescriptor.subifd = inputOptions.subifd; + } else { + throw is.invalidParameterError('subifd', 'integer between -1 and 100000', inputOptions.subifd); + } + } // Create new image if (is.defined(inputOptions.create)) { if ( @@ -255,6 +263,7 @@ function _isStreamInput () { * - `delay`: Delay in ms between each page in an animated image, provided as an array of integers. * - `pagePrimary`: Number of the primary page in a HEIF image * - `levels`: Details of each level in a multi-level image provided as an array of objects, requires libvips compiled with support for OpenSlide + * - `subifds`: Number of Sub Image File Directories in an OME-TIFF image * - `hasProfile`: Boolean indicating the presence of an embedded ICC profile * - `hasAlpha`: Boolean indicating the presence of an alpha transparency channel * - `orientation`: Number value of the EXIF Orientation header, if present diff --git a/src/common.cc b/src/common.cc index 1cd59e51..761f7a1e 100644 --- a/src/common.cc +++ b/src/common.cc @@ -104,6 +104,10 @@ namespace sharp { if (HasAttr(input, "level")) { descriptor->level = AttrAsUint32(input, "level"); } + // subIFD (OME-TIFF) + if (HasAttr(input, "subifd")) { + descriptor->subifd = AttrAsInt32(input, "subifd"); + } // Create new image if (HasAttr(input, "createChannels")) { descriptor->createChannels = AttrAsUint32(input, "createChannels"); @@ -319,6 +323,9 @@ namespace sharp { if (imageType == ImageType::OPENSLIDE) { option->set("level", descriptor->level); } + if (imageType == ImageType::TIFF) { + option->set("subifd", descriptor->subifd); + } image = VImage::new_from_buffer(descriptor->buffer, descriptor->bufferLength, nullptr, option); if (imageType == ImageType::SVG || imageType == ImageType::PDF || imageType == ImageType::MAGICK) { image = SetDensity(image, descriptor->density); @@ -391,6 +398,9 @@ namespace sharp { if (imageType == ImageType::OPENSLIDE) { option->set("level", descriptor->level); } + if (imageType == ImageType::TIFF) { + option->set("subifd", descriptor->subifd); + } image = VImage::new_from_file(descriptor->file.data(), option); if (imageType == ImageType::SVG || imageType == ImageType::PDF || imageType == ImageType::MAGICK) { image = SetDensity(image, descriptor->density); diff --git a/src/common.h b/src/common.h index 75b82804..be698cbe 100644 --- a/src/common.h +++ b/src/common.h @@ -60,6 +60,7 @@ namespace sharp { int pages; int page; int level; + int subifd; int createChannels; int createWidth; int createHeight; @@ -82,6 +83,7 @@ namespace sharp { pages(1), page(0), level(0), + subifd(-1), createChannels(0), createWidth(0), createHeight(0), diff --git a/src/metadata.cc b/src/metadata.cc index be0f2f8f..771c14f0 100644 --- a/src/metadata.cc +++ b/src/metadata.cc @@ -86,6 +86,9 @@ class MetadataWorker : public Napi::AsyncWorker { baton->levels.push_back(std::pair(width, height)); } } + if (image.get_typeof(VIPS_META_N_SUBIFDS) == G_TYPE_INT) { + baton->subifds = image.get_int(VIPS_META_N_SUBIFDS); + } baton->hasProfile = sharp::HasProfile(image); // Derived attributes baton->hasAlpha = sharp::HasAlpha(image); @@ -203,6 +206,9 @@ class MetadataWorker : public Napi::AsyncWorker { } info.Set("levels", levels); } + if (baton->subifds > 0) { + info.Set("subifds", baton->subifds); + } info.Set("hasProfile", baton->hasProfile); info.Set("hasAlpha", baton->hasAlpha); if (baton->orientation > 0) { diff --git a/src/metadata.h b/src/metadata.h index da9fc80c..2262cd37 100644 --- a/src/metadata.h +++ b/src/metadata.h @@ -41,6 +41,7 @@ struct MetadataBaton { int pagePrimary; std::string compression; std::vector> levels; + int subifds; bool hasProfile; bool hasAlpha; int orientation; @@ -68,6 +69,7 @@ struct MetadataBaton { pageHeight(0), loop(-1), pagePrimary(-1), + subifds(0), hasProfile(false), hasAlpha(false), orientation(0), diff --git a/test/unit/io.js b/test/unit/io.js index fa855471..a3d6c816 100644 --- a/test/unit/io.js +++ b/test/unit/io.js @@ -718,6 +718,19 @@ describe('Input/output', function () { sharp({ level: -1 }); }, /Expected integer between 0 and 256 for level but received -1 of type number/); }); + it('Valid subifd property', function () { + sharp({ subifd: 1 }); + }); + it('Invalid subifd property (string) throws', function () { + assert.throws(function () { + sharp({ subifd: '1' }); + }, /Expected integer between -1 and 100000 for subifd but received 1 of type string/); + }); + it('Invalid subifd property (float) throws', function () { + assert.throws(function () { + sharp({ subifd: 1.2 }); + }, /Expected integer between -1 and 100000 for subifd but received 1.2 of type number/); + }); }); describe('create new image', function () {