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/
+ );
+ });
});