diff --git a/docs/src/content/docs/api-constructor.md b/docs/src/content/docs/api-constructor.md index 8010b58b..b5b7a9c8 100644 --- a/docs/src/content/docs/api-constructor.md +++ b/docs/src/content/docs/api-constructor.md @@ -47,6 +47,7 @@ where the overall height is the `pageHeight` multiplied by the number of `pages` | [options.subifd] | number | -1 | subIFD (Sub Image File Directory) to extract for OME-TIFF, defaults to main image. | | [options.level] | number | 0 | level to extract from a multi-level input (OpenSlide), zero based. | | [options.pdfBackground] | string \| Object | | Background colour to use when PDF is partially transparent. Parsed by the [color](https://www.npmjs.org/package/color) module to extract values for red, green, blue and alpha. Requires the use of a globally-installed libvips compiled with support for PDFium, Poppler, ImageMagick or GraphicsMagick. | +| [options.jp2Oneshot] | boolean | false | Set to `true` to decode tiled JPEG 2000 images in a single operation, improving compatibility. | | [options.animated] | boolean | false | Set to `true` to read all frames/pages of an animated image (GIF, WebP, TIFF), equivalent of setting `pages` to `-1`. | | [options.raw] | Object | | describes raw pixel input image data. See `raw()` for pixel ordering. | | [options.raw.width] | number | | integral number of pixels wide. | diff --git a/docs/src/content/docs/changelog.md b/docs/src/content/docs/changelog.md index 191a0a72..f84859d8 100644 --- a/docs/src/content/docs/changelog.md +++ b/docs/src/content/docs/changelog.md @@ -10,6 +10,10 @@ Requires libvips v8.17.0 * Upgrade to libvips v8.17.0 for upstream bug fixes. +* Expose JPEG 2000 `oneshot` decoder option. + [#4262](https://github.com/lovell/sharp/pull/4262) + [@mbklein](https://github.com/mbklein) + * Support composite operation with non-sRGB pipeline colourspace. [#4412](https://github.com/lovell/sharp/pull/4412) [@kleisauke](https://github.com/kleisauke) diff --git a/lib/constructor.js b/lib/constructor.js index d9490068..c70bbf1e 100644 --- a/lib/constructor.js +++ b/lib/constructor.js @@ -156,6 +156,7 @@ const debuglog = util.debuglog('sharp'); * @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 {string|Object} [options.pdfBackground] - Background colour to use when PDF is partially transparent. Parsed by the [color](https://www.npmjs.org/package/color) module to extract values for red, green, blue and alpha. Requires the use of a globally-installed libvips compiled with support for PDFium, Poppler, ImageMagick or GraphicsMagick. + * @param {boolean} [options.jp2Oneshot=false] - Set to `true` to decode tiled JPEG 2000 images in a single operation, improving compatibility. * @param {boolean} [options.animated=false] - Set to `true` to read all frames/pages of an animated image (GIF, WebP, TIFF), equivalent of setting `pages` to `-1`. * @param {Object} [options.raw] - describes raw pixel input image data. See `raw()` for pixel ordering. * @param {number} [options.raw.width] - integral number of pixels wide. diff --git a/lib/index.d.ts b/lib/index.d.ts index a8f1d98d..9ed7190d 100644 --- a/lib/index.d.ts +++ b/lib/index.d.ts @@ -1009,6 +1009,8 @@ declare namespace sharp { level?: number | undefined; /** Background colour to use when PDF is partially transparent. Requires the use of a globally-installed libvips compiled with support for PDFium, Poppler, ImageMagick or GraphicsMagick. */ pdfBackground?: Colour | Color | undefined; + /** Set to `true` to load JPEG 2000 images using [oneshot mode](https://github.com/libvips/libvips/issues/4205) */ + jp2Oneshot?: boolean | undefined; /** Set to `true` to read all frames/pages of an animated image (equivalent of setting `pages` to `-1`). (optional, default false) */ animated?: boolean | undefined; /** Describes raw pixel input image data. See raw() for pixel ordering. */ diff --git a/lib/input.js b/lib/input.js index d9d29f3a..59de9a6c 100644 --- a/lib/input.js +++ b/lib/input.js @@ -27,9 +27,9 @@ const align = { * @private */ function _inputOptionsFromObject (obj) { - const { raw, density, limitInputPixels, ignoreIcc, unlimited, sequentialRead, failOn, failOnError, animated, page, pages, subifd, pdfBackground, autoOrient } = obj; - return [raw, density, limitInputPixels, ignoreIcc, unlimited, sequentialRead, failOn, failOnError, animated, page, pages, subifd, pdfBackground, autoOrient].some(is.defined) - ? { raw, density, limitInputPixels, ignoreIcc, unlimited, sequentialRead, failOn, failOnError, animated, page, pages, subifd, pdfBackground, autoOrient } + const { raw, density, limitInputPixels, ignoreIcc, unlimited, sequentialRead, failOn, failOnError, animated, page, pages, subifd, pdfBackground, autoOrient, jp2Oneshot } = obj; + return [raw, density, limitInputPixels, ignoreIcc, unlimited, sequentialRead, failOn, failOnError, animated, page, pages, subifd, pdfBackground, autoOrient, jp2Oneshot].some(is.defined) + ? { raw, density, limitInputPixels, ignoreIcc, unlimited, sequentialRead, failOn, failOnError, animated, page, pages, subifd, pdfBackground, autoOrient, jp2Oneshot } : undefined; } @@ -250,6 +250,14 @@ function _createInputDescriptor (input, inputOptions, containerOptions) { if (is.defined(inputOptions.pdfBackground)) { inputDescriptor.pdfBackground = this._getBackgroundColourOption(inputOptions.pdfBackground); } + // JP2 oneshot + if (is.defined(inputOptions.jp2Oneshot)) { + if (is.bool(inputOptions.jp2Oneshot)) { + inputDescriptor.jp2Oneshot = inputOptions.jp2Oneshot; + } else { + throw is.invalidParameterError('jp2Oneshot', 'boolean', inputOptions.jp2Oneshot); + } + } // Create new image if (is.defined(inputOptions.create)) { if ( diff --git a/src/common.cc b/src/common.cc index abbfe705..57ae4aef 100644 --- a/src/common.cc +++ b/src/common.cc @@ -113,6 +113,10 @@ namespace sharp { if (HasAttr(input, "pdfBackground")) { descriptor->pdfBackground = AttrAsVectorOfDouble(input, "pdfBackground"); } + // Use JPEG 2000 oneshot mode? + if (HasAttr(input, "jp2Oneshot")) { + descriptor->jp2Oneshot = AttrAsBool(input, "jp2Oneshot"); + } // Create new image if (HasAttr(input, "createChannels")) { descriptor->createChannels = AttrAsUint32(input, "createChannels"); @@ -434,6 +438,9 @@ namespace sharp { if (imageType == ImageType::PDF) { option->set("background", descriptor->pdfBackground); } + if (imageType == ImageType::JP2) { + option->set("oneshot", descriptor->jp2Oneshot); + } 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); @@ -541,6 +548,9 @@ namespace sharp { if (imageType == ImageType::PDF) { option->set("background", descriptor->pdfBackground); } + if (imageType == ImageType::JP2) { + option->set("oneshot", descriptor->jp2Oneshot); + } 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 3031c629..b926972b 100644 --- a/src/common.h +++ b/src/common.h @@ -78,6 +78,7 @@ namespace sharp { VipsAlign joinHalign; VipsAlign joinValign; std::vector pdfBackground; + bool jp2Oneshot; InputDescriptor(): autoOrient(false), @@ -120,7 +121,8 @@ namespace sharp { joinBackground{ 0.0, 0.0, 0.0, 255.0 }, joinHalign(VIPS_ALIGN_LOW), joinValign(VIPS_ALIGN_LOW), - pdfBackground{ 255.0, 255.0, 255.0, 255.0 } {} + pdfBackground{ 255.0, 255.0, 255.0, 255.0 }, + jp2Oneshot(false) {} }; // Convenience methods to access the attributes of a Napi::Object diff --git a/test/fixtures/index.js b/test/fixtures/index.js index f5b686ad..b69d6465 100644 --- a/test/fixtures/index.js +++ b/test/fixtures/index.js @@ -117,6 +117,7 @@ module.exports = { inputTiffFogra: getPath('fogra-0-100-100-0.tif'), // https://github.com/lovell/sharp/issues/4045 inputJp2: getPath('relax.jp2'), // https://www.fnordware.com/j2k/relax.jp2 + inputJp2TileParts: getPath('relax_tileparts.jp2'), // kdu_expand -i relax.jp2 -o relax-tmp.tif ; kdu_compress -i relax-tmp.tif -o relax_tileparts.jp2 -jp2_space sRGB Clayers=8 -rate 1.0,0.04 Stiles='{128,128}' ORGtparts=L ; rm relax-tmp.tif inputGif: getPath('Crash_test.gif'), // http://upload.wikimedia.org/wikipedia/commons/e/e3/Crash_test.gif inputGifGreyPlusAlpha: getPath('grey-plus-alpha.gif'), // http://i.imgur.com/gZ5jlmE.gif inputGifAnimated: getPath('rotating-squares.gif'), // CC0 https://loading.io/spinner/blocks/-rotating-squares-preloader-gif diff --git a/test/fixtures/relax_tileparts.jp2 b/test/fixtures/relax_tileparts.jp2 new file mode 100644 index 00000000..62621c68 Binary files /dev/null and b/test/fixtures/relax_tileparts.jp2 differ diff --git a/test/types/sharp.test-d.ts b/test/types/sharp.test-d.ts index d968e07c..88e5b45b 100644 --- a/test/types/sharp.test-d.ts +++ b/test/types/sharp.test-d.ts @@ -721,6 +721,9 @@ const color: sharp.Color = '#fff'; sharp({ pdfBackground: colour }); sharp({ pdfBackground: color }); +sharp({ jp2Oneshot: true }); +sharp({ jp2Oneshot: false }); + sharp({ autoOrient: true }); sharp({ autoOrient: false }); sharp().autoOrient(); diff --git a/test/unit/jp2.js b/test/unit/jp2.js index c70f1543..ff04fe72 100644 --- a/test/unit/jp2.js +++ b/test/unit/jp2.js @@ -93,10 +93,38 @@ describe('JP2 output', () => { }); }); - it('Invalid JP2 chromaSubsampling value throws error', function () { - assert.throws(function () { - sharp().jpeg({ chromaSubsampling: '4:2:2' }); + it('can use the jp2Oneshot option to handle multi-part tiled JPEG 2000 file', async () => { + const outputJpg = fixtures.path('output.jpg'); + await assert.rejects( + () => sharp(fixtures.inputJp2TileParts).toFile(outputJpg) + ); + await assert.doesNotReject(async () => { + await sharp(fixtures.inputJp2TileParts, { jp2Oneshot: true }).toFile(outputJpg); + const { format, width, height } = await sharp(outputJpg).metadata(); + assert.strictEqual(format, 'jpeg'); + assert.strictEqual(width, 320); + assert.strictEqual(height, 240); }); }); + + it('Invalid JP2 chromaSubsampling value throws error', () => { + assert.throws( + () => sharp().jp2({ chromaSubsampling: '4:2:2' }), + /Expected one of 4:2:0, 4:4:4 but received 4:2:2 of type string/ + ); + }); } + + it('valid JP2 oneshot value does not throw error', () => { + assert.doesNotThrow( + () => sharp(fixtures.inputJp2TileParts, { jp2Oneshot: true }) + ); + }); + + it('invalid JP2 oneshot value throws error', () => { + assert.throws( + () => sharp(fixtures.inputJp2TileParts, { jp2Oneshot: 'fail' }), + /Expected boolean for jp2Oneshot but received fail of type string/ + ); + }); });