diff --git a/docs/api-constructor.md b/docs/api-constructor.md index 32a79a011..65c0cd90a 100644 --- a/docs/api-constructor.md +++ b/docs/api-constructor.md @@ -41,6 +41,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 load JPEG 2000 images using [oneshot mode](https://github.com/libvips/libvips/issues/4205)| | [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/lib/constructor.js b/lib/constructor.js index 0ff85c667..93eb81ac1 100644 --- a/lib/constructor.js +++ b/lib/constructor.js @@ -140,6 +140,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 load JPEG 2000 images using [oneshot mode](https://github.com/libvips/libvips/issues/4205) * @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. @@ -295,6 +296,7 @@ const Sharp = function (input, options) { jp2TileWidth: 512, jp2Lossless: false, jp2ChromaSubsampling: '4:4:4', + jp2Oneshot: false, webpQuality: 80, webpAlphaQuality: 100, webpLossless: false, diff --git a/lib/index.d.ts b/lib/index.d.ts index 77415e235..cd9e78128 100644 --- a/lib/index.d.ts +++ b/lib/index.d.ts @@ -938,6 +938,8 @@ declare namespace sharp { /** 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 read all frames/pages of an animated image (equivalent of setting `pages` to `-1`). (optional, default false) */ + jp2Oneshot?: boolean | undefined; + /** Set to `true` to load JPEG 2000 images using [oneshot mode](https://github.com/libvips/libvips/issues/4205) **/ animated?: boolean | undefined; /** Describes raw pixel input image data. See raw() for pixel ordering. */ raw?: CreateRaw | undefined; diff --git a/lib/input.js b/lib/input.js index abb10e53a..e5cca83da 100644 --- a/lib/input.js +++ b/lib/input.js @@ -24,9 +24,9 @@ const align = { * @private */ function _inputOptionsFromObject (obj) { - const { raw, density, limitInputPixels, ignoreIcc, unlimited, sequentialRead, failOn, failOnError, animated, page, pages, subifd, pdfBackground } = obj; - return [raw, density, limitInputPixels, ignoreIcc, unlimited, sequentialRead, failOn, failOnError, animated, page, pages, subifd, pdfBackground].some(is.defined) - ? { raw, density, limitInputPixels, ignoreIcc, unlimited, sequentialRead, failOn, failOnError, animated, page, pages, subifd, pdfBackground } + const { raw, density, limitInputPixels, ignoreIcc, unlimited, sequentialRead, failOn, failOnError, animated, page, pages, subifd, pdfBackground, jp2Oneshot } = obj; + return [raw, density, limitInputPixels, ignoreIcc, unlimited, sequentialRead, failOn, failOnError, animated, page, pages, subifd, pdfBackground, jp2Oneshot].some(is.defined) + ? { raw, density, limitInputPixels, ignoreIcc, unlimited, sequentialRead, failOn, failOnError, animated, page, pages, subifd, pdfBackground, jp2Oneshot } : undefined; } @@ -40,7 +40,8 @@ function _createInputDescriptor (input, inputOptions, containerOptions) { limitInputPixels: Math.pow(0x3FFF, 2), ignoreIcc: false, unlimited: false, - sequentialRead: true + sequentialRead: true, + jp2Oneshot: false }; if (is.string(input)) { // filesystem @@ -226,6 +227,15 @@ function _createInputDescriptor (input, inputOptions, containerOptions) { if (is.defined(inputOptions.pdfBackground)) { this._setBackgroundColourOption('pdfBackground', inputOptions.pdfBackground); } + + 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 1d6a8f632..d7a635d65 100644 --- a/src/common.cc +++ b/src/common.cc @@ -113,6 +113,11 @@ 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"); @@ -409,6 +414,9 @@ namespace sharp { if (imageType == ImageType::PDF) { option->set("background", descriptor->pdfBackground); } + if (imageType == ImageType::JP2 && descriptor->jp2Oneshot) { + option->set("oneshot", 1); + } 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); @@ -516,6 +524,9 @@ namespace sharp { if (imageType == ImageType::PDF) { option->set("background", descriptor->pdfBackground); } + if (imageType == ImageType::JP2 && descriptor->jp2Oneshot) { + option->set("oneshot", 1); + } 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 2f983d9db..a8748bf66 100644 --- a/src/common.h +++ b/src/common.h @@ -75,6 +75,7 @@ namespace sharp { VipsTextWrap textWrap; int textAutofitDpi; std::vector pdfBackground; + bool jp2Oneshot; InputDescriptor(): buffer(nullptr), @@ -110,7 +111,8 @@ namespace sharp { textSpacing(0), textWrap(VIPS_TEXT_WRAP_WORD), textAutofitDpi(0), - 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 e4b8e266e..24796a634 100644 --- a/test/fixtures/index.js +++ b/test/fixtures/index.js @@ -36,138 +36,154 @@ async function fingerprint (image) { } module.exports = { - - inputJpgWithLandscapeExif1: getPath('Landscape_1.jpg'), // https://github.com/recurser/exif-orientation-examples - inputJpgWithLandscapeExif2: getPath('Landscape_2.jpg'), // https://github.com/recurser/exif-orientation-examples - inputJpgWithLandscapeExif3: getPath('Landscape_3.jpg'), // https://github.com/recurser/exif-orientation-examples - inputJpgWithLandscapeExif4: getPath('Landscape_4.jpg'), // https://github.com/recurser/exif-orientation-examples - inputJpgWithLandscapeExif5: getPath('Landscape_5.jpg'), // https://github.com/recurser/exif-orientation-examples - inputJpgWithLandscapeExif6: getPath('Landscape_6.jpg'), // https://github.com/recurser/exif-orientation-examples - inputJpgWithLandscapeExif7: getPath('Landscape_7.jpg'), // https://github.com/recurser/exif-orientation-examples - inputJpgWithLandscapeExif8: getPath('Landscape_8.jpg'), // https://github.com/recurser/exif-orientation-examples - - inputJpgWithPortraitExif1: getPath('Portrait_1.jpg'), // https://github.com/recurser/exif-orientation-examples - inputJpgWithPortraitExif2: getPath('Portrait_2.jpg'), // https://github.com/recurser/exif-orientation-examples - inputJpgWithPortraitExif3: getPath('Portrait_3.jpg'), // https://github.com/recurser/exif-orientation-examples - inputJpgWithPortraitExif4: getPath('Portrait_4.jpg'), // https://github.com/recurser/exif-orientation-examples - inputJpgWithPortraitExif5: getPath('Portrait_5.jpg'), // https://github.com/recurser/exif-orientation-examples - inputJpgWithPortraitExif6: getPath('Portrait_6.jpg'), // https://github.com/recurser/exif-orientation-examples - inputJpgWithPortraitExif7: getPath('Portrait_7.jpg'), // https://github.com/recurser/exif-orientation-examples - inputJpgWithPortraitExif8: getPath('Portrait_8.jpg'), // https://github.com/recurser/exif-orientation-examples - - inputJpg: getPath('2569067123_aca715a2ee_o.jpg'), // http://www.flickr.com/photos/grizdave/2569067123/ - inputJpgWithExif: getPath('Landscape_8.jpg'), // https://github.com/recurser/exif-orientation-examples/blob/master/Landscape_8.jpg - inputJpgWithIptcAndXmp: getPath('Landscape_9.jpg'), // https://unsplash.com/photos/RWAIyGmgHTQ - inputJpgWithExifMirroring: getPath('Landscape_5.jpg'), // https://github.com/recurser/exif-orientation-examples/blob/master/Landscape_5.jpg - inputJpgWithGammaHoliness: getPath('gamma_dalai_lama_gray.jpg'), // http://www.4p8.com/eric.brasseur/gamma.html - inputJpgWithCmykProfile: getPath('Channel_digital_image_CMYK_color.jpg'), // http://en.wikipedia.org/wiki/File:Channel_digital_image_CMYK_color.jpg - inputJpgWithCmykNoProfile: getPath('Channel_digital_image_CMYK_color_no_profile.jpg'), - inputJpgWithCorruptHeader: getPath('corrupt-header.jpg'), - inputJpgWithLowContrast: getPath('low-contrast.jpg'), // http://www.flickr.com/photos/grizdave/2569067123/ - inputJpgLarge: getPath('giant-image.jpg'), - inputJpg320x240: getPath('320x240.jpg'), // http://www.andrewault.net/2010/01/26/create-a-test-pattern-video-with-perl/ - inputJpgOverlayLayer2: getPath('alpha-layer-2-ink.jpg'), - inputJpgTruncated: getPath('truncated.jpg'), // head -c 10000 2569067123_aca715a2ee_o.jpg > truncated.jpg - inputJpgCenteredImage: getPath('centered_image.jpeg'), - inputJpgRandom: getPath('random.jpg'), // convert -size 200x200 xc: +noise Random random.jpg - inputJpgThRandom: getPath('thRandom.jpg'), // convert random.jpg -channel G -threshold 5% -separate +channel -negate thRandom.jpg - inputJpgLossless: getPath('testimgl.jpg'), // Lossless JPEG from ftp://ftp.fu-berlin.de/unix/X11/graphics/ImageMagick/delegates/ljpeg-6b.tar.gz - - inputPng: getPath('50020484-00001.png'), // http://c.searspartsdirect.com/lis_png/PLDM/50020484-00001.png - inputPngGradients: getPath('gradients-rgb8.png'), - inputPngWithTransparency: getPath('blackbug.png'), // public domain - inputPngCompleteTransparency: getPath('full-transparent.png'), - inputPngWithGreyAlpha: getPath('grey-8bit-alpha.png'), - inputPngWithOneColor: getPath('2x2_fdcce6.png'), - inputPngWithTransparency16bit: getPath('tbgn2c16.png'), // http://www.schaik.com/pngsuite/tbgn2c16.png - inputPng16BitGreyAlpha: getPath('16-bit-grey-alpha.png'), // CC-BY-NC-SA florc http://www.colourlovers.com/pattern/50713/pat - inputPngOverlayLayer0: getPath('alpha-layer-0-background.png'), - inputPngOverlayLayer1: getPath('alpha-layer-1-fill.png'), - inputPngAlphaPremultiplicationSmall: getPath('alpha-premultiply-1024x768-paper.png'), - inputPngAlphaPremultiplicationLarge: getPath('alpha-premultiply-2048x1536-paper.png'), - inputPngBooleanNoAlpha: getPath('bandbool.png'), - inputPngTestJoinChannel: getPath('testJoinChannel.png'), - inputPngTruncated: getPath('truncated.png'), // gm convert 2569067123_aca715a2ee_o.jpg -resize 320x240 saw.png ; head -c 10000 saw.png > truncated.png - inputPngEmbed: getPath('embedgravitybird.png'), // Released to sharp under a CC BY 4.0 - inputPngRGBWithAlpha: getPath('2569067123_aca715a2ee_o.png'), // http://www.flickr.com/photos/grizdave/2569067123/ (same as inputJpg) - inputPngImageInAlpha: getPath('image-in-alpha.png'), // https://github.com/lovell/sharp/issues/1597 - inputPngSolidAlpha: getPath('with-alpha.png'), // https://github.com/lovell/sharp/issues/1599 - inputPngP3: getPath('p3.png'), // https://github.com/lovell/sharp/issues/2862 - inputPngPalette: getPath('swiss.png'), // https://github.com/randy408/libspng/issues/188 - inputPngTrimIncludeAlpha: getPath('trim-mc.png'), // https://github.com/lovell/sharp/issues/2166 - inputPngTrimSpecificColour: getPath('Flag_of_the_Netherlands.png'), // https://commons.wikimedia.org/wiki/File:Flag_of_the_Netherlands.svg - inputPngTrimSpecificColour16bit: getPath('Flag_of_the_Netherlands-16bit.png'), // convert Flag_of_the_Netherlands.png -depth 16 Flag_of_the_Netherlands-16bit.png - inputPngTrimSpecificColourIncludeAlpha: getPath('Flag_of_the_Netherlands-alpha.png'), // convert Flag_of_the_Netherlands.png -alpha set -background none -channel A -evaluate multiply 0.5 +channel Flag_of_the_Netherlands-alpha.png - inputPngUint32Limit: getPath('65536-uint32-limit.png'), // https://alexandre.alapetite.fr/doc-alex/large-image/ - inputPngWithProPhotoProfile: getPath('prophoto.png'), - - inputWebP: getPath('4.webp'), // http://www.gstatic.com/webp/gallery/4.webp - inputWebPWithTransparency: getPath('5_webp_a.webp'), // http://www.gstatic.com/webp/gallery3/5_webp_a.webp - inputWebPAnimated: getPath('rotating-squares.webp'), // http://www.gstatic.com/webp/gallery3/5_webp_a.webp - inputWebPAnimatedLoop3: getPath('animated-loop-3.webp'), // http://www.gstatic.com/webp/gallery3/5_webp_a.webp - inputWebPAnimatedBigHeight: getPath('big-height.webp'), - inputTiff: getPath('G31D.TIF'), // http://www.fileformat.info/format/tiff/sample/e6c9a6e5253348f4aef6d17b534360ab/index.htm - inputTiffMultipage: getPath('G31D_MULTI.TIF'), // gm convert G31D.TIF -resize 50% G31D_2.TIF ; tiffcp G31D.TIF G31D_2.TIF G31D_MULTI.TIF - inputTiffCielab: getPath('cielab-dagams.tiff'), // https://github.com/lovell/sharp/issues/646 - inputTiffUncompressed: getPath('uncompressed_tiff.tiff'), // https://code.google.com/archive/p/imagetestsuite/wikis/TIFFTestSuite.wiki file: 0c84d07e1b22b76f24cccc70d8788e4a.tif - inputTiff8BitDepth: getPath('8bit_depth.tiff'), - inputTifftagPhotoshop: getPath('tifftag-photoshop.tiff'), // https://github.com/lovell/sharp/issues/1600 - 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 - 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 - inputGifAnimatedLoop3: getPath('animated-loop-3.gif'), // CC-BY-SA-4.0 Petrus3743 https://commons.wikimedia.org/wiki/File:01-Goldener_Schnitt_Formel-Animation.gif - inputSvg: getPath('check.svg'), // http://dev.w3.org/SVG/tools/svgweb/samples/svg-files/check.svg - inputSvgSmallViewBox: getPath('circle.svg'), - inputSvgWithEmbeddedImages: getPath('struct-image-04-t.svg'), // https://dev.w3.org/SVG/profiles/1.2T/test/svg/struct-image-04-t.svg - inputAvif: getPath('sdr_cosmos12920_cicp1-13-6_yuv444_full_qp10.avif'), // CC by-nc-nd https://github.com/AOMediaCodec/av1-avif/tree/master/testFiles/Netflix - - inputJPGBig: getPath('flowers.jpeg'), - - inputPngStripesV: getPath('stripesV.png'), - inputPngStripesH: getPath('stripesH.png'), - - inputJpgBooleanTest: getPath('booleanTest.jpg'), - - inputV: getPath('vfile.v'), - - inputJpgClahe: getPath('concert.jpg'), // public domain - https://www.flickr.com/photos/mars_/14389236779/ - - testPattern: getPath('test-pattern.png'), - - inputPngWithTransparent: getPath('d.png'), + inputJpgWithLandscapeExif1: getPath("Landscape_1.jpg"), // https://github.com/recurser/exif-orientation-examples + inputJpgWithLandscapeExif2: getPath("Landscape_2.jpg"), // https://github.com/recurser/exif-orientation-examples + inputJpgWithLandscapeExif3: getPath("Landscape_3.jpg"), // https://github.com/recurser/exif-orientation-examples + inputJpgWithLandscapeExif4: getPath("Landscape_4.jpg"), // https://github.com/recurser/exif-orientation-examples + inputJpgWithLandscapeExif5: getPath("Landscape_5.jpg"), // https://github.com/recurser/exif-orientation-examples + inputJpgWithLandscapeExif6: getPath("Landscape_6.jpg"), // https://github.com/recurser/exif-orientation-examples + inputJpgWithLandscapeExif7: getPath("Landscape_7.jpg"), // https://github.com/recurser/exif-orientation-examples + inputJpgWithLandscapeExif8: getPath("Landscape_8.jpg"), // https://github.com/recurser/exif-orientation-examples + + inputJpgWithPortraitExif1: getPath("Portrait_1.jpg"), // https://github.com/recurser/exif-orientation-examples + inputJpgWithPortraitExif2: getPath("Portrait_2.jpg"), // https://github.com/recurser/exif-orientation-examples + inputJpgWithPortraitExif3: getPath("Portrait_3.jpg"), // https://github.com/recurser/exif-orientation-examples + inputJpgWithPortraitExif4: getPath("Portrait_4.jpg"), // https://github.com/recurser/exif-orientation-examples + inputJpgWithPortraitExif5: getPath("Portrait_5.jpg"), // https://github.com/recurser/exif-orientation-examples + inputJpgWithPortraitExif6: getPath("Portrait_6.jpg"), // https://github.com/recurser/exif-orientation-examples + inputJpgWithPortraitExif7: getPath("Portrait_7.jpg"), // https://github.com/recurser/exif-orientation-examples + inputJpgWithPortraitExif8: getPath("Portrait_8.jpg"), // https://github.com/recurser/exif-orientation-examples + + inputJpg: getPath("2569067123_aca715a2ee_o.jpg"), // http://www.flickr.com/photos/grizdave/2569067123/ + inputJpgWithExif: getPath("Landscape_8.jpg"), // https://github.com/recurser/exif-orientation-examples/blob/master/Landscape_8.jpg + inputJpgWithIptcAndXmp: getPath("Landscape_9.jpg"), // https://unsplash.com/photos/RWAIyGmgHTQ + inputJpgWithExifMirroring: getPath("Landscape_5.jpg"), // https://github.com/recurser/exif-orientation-examples/blob/master/Landscape_5.jpg + inputJpgWithGammaHoliness: getPath("gamma_dalai_lama_gray.jpg"), // http://www.4p8.com/eric.brasseur/gamma.html + inputJpgWithCmykProfile: getPath("Channel_digital_image_CMYK_color.jpg"), // http://en.wikipedia.org/wiki/File:Channel_digital_image_CMYK_color.jpg + inputJpgWithCmykNoProfile: getPath( + "Channel_digital_image_CMYK_color_no_profile.jpg" + ), + inputJpgWithCorruptHeader: getPath("corrupt-header.jpg"), + inputJpgWithLowContrast: getPath("low-contrast.jpg"), // http://www.flickr.com/photos/grizdave/2569067123/ + inputJpgLarge: getPath("giant-image.jpg"), + inputJpg320x240: getPath("320x240.jpg"), // http://www.andrewault.net/2010/01/26/create-a-test-pattern-video-with-perl/ + inputJpgOverlayLayer2: getPath("alpha-layer-2-ink.jpg"), + inputJpgTruncated: getPath("truncated.jpg"), // head -c 10000 2569067123_aca715a2ee_o.jpg > truncated.jpg + inputJpgCenteredImage: getPath("centered_image.jpeg"), + inputJpgRandom: getPath("random.jpg"), // convert -size 200x200 xc: +noise Random random.jpg + inputJpgThRandom: getPath("thRandom.jpg"), // convert random.jpg -channel G -threshold 5% -separate +channel -negate thRandom.jpg + inputJpgLossless: getPath("testimgl.jpg"), // Lossless JPEG from ftp://ftp.fu-berlin.de/unix/X11/graphics/ImageMagick/delegates/ljpeg-6b.tar.gz + + inputPng: getPath("50020484-00001.png"), // http://c.searspartsdirect.com/lis_png/PLDM/50020484-00001.png + inputPngGradients: getPath("gradients-rgb8.png"), + inputPngWithTransparency: getPath("blackbug.png"), // public domain + inputPngCompleteTransparency: getPath("full-transparent.png"), + inputPngWithGreyAlpha: getPath("grey-8bit-alpha.png"), + inputPngWithOneColor: getPath("2x2_fdcce6.png"), + inputPngWithTransparency16bit: getPath("tbgn2c16.png"), // http://www.schaik.com/pngsuite/tbgn2c16.png + inputPng16BitGreyAlpha: getPath("16-bit-grey-alpha.png"), // CC-BY-NC-SA florc http://www.colourlovers.com/pattern/50713/pat + inputPngOverlayLayer0: getPath("alpha-layer-0-background.png"), + inputPngOverlayLayer1: getPath("alpha-layer-1-fill.png"), + inputPngAlphaPremultiplicationSmall: getPath( + "alpha-premultiply-1024x768-paper.png" + ), + inputPngAlphaPremultiplicationLarge: getPath( + "alpha-premultiply-2048x1536-paper.png" + ), + inputPngBooleanNoAlpha: getPath("bandbool.png"), + inputPngTestJoinChannel: getPath("testJoinChannel.png"), + inputPngTruncated: getPath("truncated.png"), // gm convert 2569067123_aca715a2ee_o.jpg -resize 320x240 saw.png ; head -c 10000 saw.png > truncated.png + inputPngEmbed: getPath("embedgravitybird.png"), // Released to sharp under a CC BY 4.0 + inputPngRGBWithAlpha: getPath("2569067123_aca715a2ee_o.png"), // http://www.flickr.com/photos/grizdave/2569067123/ (same as inputJpg) + inputPngImageInAlpha: getPath("image-in-alpha.png"), // https://github.com/lovell/sharp/issues/1597 + inputPngSolidAlpha: getPath("with-alpha.png"), // https://github.com/lovell/sharp/issues/1599 + inputPngP3: getPath("p3.png"), // https://github.com/lovell/sharp/issues/2862 + inputPngPalette: getPath("swiss.png"), // https://github.com/randy408/libspng/issues/188 + inputPngTrimIncludeAlpha: getPath("trim-mc.png"), // https://github.com/lovell/sharp/issues/2166 + inputPngTrimSpecificColour: getPath("Flag_of_the_Netherlands.png"), // https://commons.wikimedia.org/wiki/File:Flag_of_the_Netherlands.svg + inputPngTrimSpecificColour16bit: getPath("Flag_of_the_Netherlands-16bit.png"), // convert Flag_of_the_Netherlands.png -depth 16 Flag_of_the_Netherlands-16bit.png + inputPngTrimSpecificColourIncludeAlpha: getPath( + "Flag_of_the_Netherlands-alpha.png" + ), // convert Flag_of_the_Netherlands.png -alpha set -background none -channel A -evaluate multiply 0.5 +channel Flag_of_the_Netherlands-alpha.png + inputPngUint32Limit: getPath("65536-uint32-limit.png"), // https://alexandre.alapetite.fr/doc-alex/large-image/ + inputPngWithProPhotoProfile: getPath("prophoto.png"), + + inputWebP: getPath("4.webp"), // http://www.gstatic.com/webp/gallery/4.webp + inputWebPWithTransparency: getPath("5_webp_a.webp"), // http://www.gstatic.com/webp/gallery3/5_webp_a.webp + inputWebPAnimated: getPath("rotating-squares.webp"), // http://www.gstatic.com/webp/gallery3/5_webp_a.webp + inputWebPAnimatedLoop3: getPath("animated-loop-3.webp"), // http://www.gstatic.com/webp/gallery3/5_webp_a.webp + inputWebPAnimatedBigHeight: getPath("big-height.webp"), + inputTiff: getPath("G31D.TIF"), // http://www.fileformat.info/format/tiff/sample/e6c9a6e5253348f4aef6d17b534360ab/index.htm + inputTiffMultipage: getPath("G31D_MULTI.TIF"), // gm convert G31D.TIF -resize 50% G31D_2.TIF ; tiffcp G31D.TIF G31D_2.TIF G31D_MULTI.TIF + inputTiffCielab: getPath("cielab-dagams.tiff"), // https://github.com/lovell/sharp/issues/646 + inputTiffUncompressed: getPath("uncompressed_tiff.tiff"), // https://code.google.com/archive/p/imagetestsuite/wikis/TIFFTestSuite.wiki file: 0c84d07e1b22b76f24cccc70d8788e4a.tif + inputTiff8BitDepth: getPath("8bit_depth.tiff"), + inputTifftagPhotoshop: getPath("tifftag-photoshop.tiff"), // https://github.com/lovell/sharp/issues/1600 + 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 + inputGifAnimatedLoop3: getPath("animated-loop-3.gif"), // CC-BY-SA-4.0 Petrus3743 https://commons.wikimedia.org/wiki/File:01-Goldener_Schnitt_Formel-Animation.gif + inputSvg: getPath("check.svg"), // http://dev.w3.org/SVG/tools/svgweb/samples/svg-files/check.svg + inputSvgSmallViewBox: getPath("circle.svg"), + inputSvgWithEmbeddedImages: getPath("struct-image-04-t.svg"), // https://dev.w3.org/SVG/profiles/1.2T/test/svg/struct-image-04-t.svg + inputAvif: getPath("sdr_cosmos12920_cicp1-13-6_yuv444_full_qp10.avif"), // CC by-nc-nd https://github.com/AOMediaCodec/av1-avif/tree/master/testFiles/Netflix + + inputJPGBig: getPath("flowers.jpeg"), + + inputPngStripesV: getPath("stripesV.png"), + inputPngStripesH: getPath("stripesH.png"), + + inputJpgBooleanTest: getPath("booleanTest.jpg"), + + inputV: getPath("vfile.v"), + + inputJpgClahe: getPath("concert.jpg"), // public domain - https://www.flickr.com/photos/mars_/14389236779/ + + testPattern: getPath("test-pattern.png"), + + inputPngWithTransparent: getPath("d.png"), // Path for tests requiring human inspection path: getPath, // Path for expected output images expected: function (filename) { - return getPath(path.join('expected', filename)); + return getPath(path.join("expected", filename)); }, // Verify similarity of expected vs actual images via fingerprint // Specify distance threshold using `options={threshold: 42}`, default // `threshold` is 5; - assertSimilar: async function (expectedImage, actualImage, options, callback) { - if (typeof options === 'function') { + assertSimilar: async function ( + expectedImage, + actualImage, + options, + callback + ) { + if (typeof options === "function") { callback = options; options = {}; } - if (typeof options === 'undefined' || options === null) { + if (typeof options === "undefined" || options === null) { options = {}; } - if (options.threshold === null || typeof options.threshold === 'undefined') { + if ( + options.threshold === null || + typeof options.threshold === "undefined" + ) { options.threshold = 5; // ~7% threshold } - if (typeof options.threshold !== 'number') { - throw new TypeError('`options.threshold` must be a number'); + if (typeof options.threshold !== "number") { + throw new TypeError("`options.threshold` must be a number"); } try { const [expectedFingerprint, actualFingerprint] = await Promise.all([ fingerprint(expectedImage), - fingerprint(actualImage) + fingerprint(actualImage), ]); let distance = 0; for (let i = 0; i < 64; i++) { @@ -176,7 +192,9 @@ module.exports = { } } if (distance > options.threshold) { - throw new Error(`Expected maximum similarity distance: ${options.threshold}. Actual: ${distance}.`); + throw new Error( + `Expected maximum similarity distance: ${options.threshold}. Actual: ${distance}.` + ); } } catch (err) { if (callback) { @@ -189,21 +207,33 @@ module.exports = { } }, - assertMaxColourDistance: function (actualImagePath, expectedImagePath, acceptedDistance) { - if (typeof actualImagePath !== 'string') { - throw new TypeError('`actualImagePath` must be a string; got ' + actualImagePath); + assertMaxColourDistance: function ( + actualImagePath, + expectedImagePath, + acceptedDistance + ) { + if (typeof actualImagePath !== "string") { + throw new TypeError( + "`actualImagePath` must be a string; got " + actualImagePath + ); } - if (typeof expectedImagePath !== 'string') { - throw new TypeError('`expectedImagePath` must be a string; got ' + expectedImagePath); + if (typeof expectedImagePath !== "string") { + throw new TypeError( + "`expectedImagePath` must be a string; got " + expectedImagePath + ); } - if (typeof acceptedDistance !== 'number') { + if (typeof acceptedDistance !== "number") { // Default threshold acceptedDistance = 1; } const distance = maxColourDistance(actualImagePath, expectedImagePath); if (distance > acceptedDistance) { - throw new Error('Expected maximum absolute distance of ' + acceptedDistance + ', actual ' + distance); + throw new Error( + "Expected maximum absolute distance of " + + acceptedDistance + + ", actual " + + distance + ); } - } - + }, }; diff --git a/test/fixtures/relax_tileparts.jp2 b/test/fixtures/relax_tileparts.jp2 new file mode 100644 index 000000000..62621c689 Binary files /dev/null and b/test/fixtures/relax_tileparts.jp2 differ diff --git a/test/unit/io.js b/test/unit/io.js index 98b96a571..07256a7aa 100644 --- a/test/unit/io.js +++ b/test/unit/io.js @@ -722,6 +722,23 @@ describe('Input/output', function () { b1: 20, b2: 25 }); + + }); + + it('can use the jp2Oneshot option to handle multi-part tiled JPEG 2000 file', async () => { + assert.rejects(async function() { + await sharp(fixtures.inputJp2TileParts) + .toFile(outputJpg); + }); + + await assert.doesNotReject(async function() { + await sharp(fixtures.inputJp2TileParts, { jp2Oneshot: true }) + .toFile(outputJpg); + const meta = await sharp(outputJpg).metadata(); + assert.strictEqual(meta.format, 'jpeg'); + assert.strictEqual(meta.width, 320); + assert.strictEqual(meta.height, 240); + }); }); describe('Switch off safety limits for certain formats', () => { @@ -914,6 +931,15 @@ describe('Input/output', function () { sharp({ pdfBackground: { red: 0, green: 255, blue: 0 } }); }, /Unable to parse color from object/); }); + it('Valid jp2Oneshot property (bool)', function () { + sharp({ jp2Oneshot: true }); + sharp({ jp2Oneshot: false }); + }) + it('Invalid jp2Oneshot property (string) throws', function () { + assert.throws(function () { + sharp({ jp2Oneshot: 'true' }); + }, /Expected boolean for jp2Oneshot but received true of type string/); + }); }); it('Fails when writing to missing directory', async () => {