diff --git a/docs/changelog.md b/docs/changelog.md index a25cdf07..d579c9dc 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -18,6 +18,8 @@ Requires libvips v8.14.0 * Prefer integer (un)premultiply for faster resizing of RGBA images. +* Add `ignoreIcc` input option to ignore embedded ICC profile. + * Allow use of GPS (IFD3) EXIF metadata. [#2767](https://github.com/lovell/sharp/issues/2767) diff --git a/lib/constructor.js b/lib/constructor.js index 7e67411f..8b51ce34 100644 --- a/lib/constructor.js +++ b/lib/constructor.js @@ -126,6 +126,7 @@ const debuglog = util.debuglog('sharp'); * @param {boolean} [options.unlimited=false] - Set this to `true` to remove safety features that help prevent memory exhaustion (JPEG, PNG, SVG, HEIF). * @param {boolean} [options.sequentialRead=true] - Set this to `false` to use random access rather than sequential read. Some operations will do this automatically. * @param {number} [options.density=72] - number representing the DPI for vector images in the range 1 to 100000. + * @param {number} [options.ignoreIcc=false] - should the embedded ICC profile, if any, be ignored. * @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. diff --git a/lib/index.d.ts b/lib/index.d.ts index 6b6e0543..a41b31b1 100644 --- a/lib/index.d.ts +++ b/lib/index.d.ts @@ -814,6 +814,8 @@ declare namespace sharp { sequentialRead?: boolean | undefined; /** Number representing the DPI for vector images in the range 1 to 100000. (optional, default 72) */ density?: number | undefined; + /** Should the embedded ICC profile, if any, be ignored. */ + ignoreIcc?: boolean | undefined; /** Number of pages to extract for multi-page input (GIF, TIFF, PDF), use -1 for all pages */ pages?: number | undefined; /** Page number to start extracting from for multi-page input (GIF, TIFF, PDF), zero based. (optional, default 0) */ diff --git a/lib/input.js b/lib/input.js index 971da50c..58c33c22 100644 --- a/lib/input.js +++ b/lib/input.js @@ -21,9 +21,9 @@ const align = { * @private */ function _inputOptionsFromObject (obj) { - const { raw, density, limitInputPixels, unlimited, sequentialRead, failOn, failOnError, animated, page, pages, subifd } = obj; - return [raw, density, limitInputPixels, unlimited, sequentialRead, failOn, failOnError, animated, page, pages, subifd].some(is.defined) - ? { raw, density, limitInputPixels, unlimited, sequentialRead, failOn, failOnError, animated, page, pages, subifd } + const { raw, density, limitInputPixels, ignoreIcc, unlimited, sequentialRead, failOn, failOnError, animated, page, pages, subifd } = obj; + return [raw, density, limitInputPixels, ignoreIcc, unlimited, sequentialRead, failOn, failOnError, animated, page, pages, subifd].some(is.defined) + ? { raw, density, limitInputPixels, ignoreIcc, unlimited, sequentialRead, failOn, failOnError, animated, page, pages, subifd } : undefined; } @@ -35,6 +35,7 @@ function _createInputDescriptor (input, inputOptions, containerOptions) { const inputDescriptor = { failOn: 'warning', limitInputPixels: Math.pow(0x3FFF, 2), + ignoreIcc: false, unlimited: false, sequentialRead: true }; @@ -97,6 +98,14 @@ function _createInputDescriptor (input, inputOptions, containerOptions) { throw is.invalidParameterError('density', 'number between 1 and 100000', inputOptions.density); } } + // Ignore embeddded ICC profile + if (is.defined(inputOptions.ignoreIcc)) { + if (is.bool(inputOptions.ignoreIcc)) { + inputDescriptor.ignoreIcc = inputOptions.ignoreIcc; + } else { + throw is.invalidParameterError('ignoreIcc', 'boolean', inputOptions.ignoreIcc); + } + } // limitInputPixels if (is.defined(inputOptions.limitInputPixels)) { if (is.bool(inputOptions.limitInputPixels)) { diff --git a/src/common.cc b/src/common.cc index 9038de2e..f8ec368b 100644 --- a/src/common.cc +++ b/src/common.cc @@ -103,6 +103,10 @@ namespace sharp { if (HasAttr(input, "density")) { descriptor->density = AttrAsDouble(input, "density"); } + // Should we ignore any embedded ICC profile + if (HasAttr(input, "ignoreIcc")) { + descriptor->ignoreIcc = AttrAsBool(input, "ignoreIcc"); + } // Raw pixel input if (HasAttr(input, "rawChannels")) { descriptor->rawDepth = AttrAsEnum(input, "rawDepth", VIPS_TYPE_BAND_FORMAT); diff --git a/src/common.h b/src/common.h index 513e9ec5..476382d0 100644 --- a/src/common.h +++ b/src/common.h @@ -55,6 +55,7 @@ namespace sharp { size_t bufferLength; bool isBuffer; double density; + bool ignoreIcc; VipsBandFormat rawDepth; int rawChannels; int rawWidth; @@ -93,6 +94,7 @@ namespace sharp { bufferLength(0), isBuffer(FALSE), density(72.0), + ignoreIcc(FALSE), rawDepth(VIPS_FORMAT_UCHAR), rawChannels(0), rawWidth(0), diff --git a/src/pipeline.cc b/src/pipeline.cc index 1732f7a4..e4d4c727 100644 --- a/src/pipeline.cc +++ b/src/pipeline.cc @@ -320,7 +320,8 @@ class PipelineWorker : public Napi::AsyncWorker { if ( sharp::HasProfile(image) && image.interpretation() != VIPS_INTERPRETATION_LABS && - image.interpretation() != VIPS_INTERPRETATION_GREY16 + image.interpretation() != VIPS_INTERPRETATION_GREY16 && + !baton->input->ignoreIcc ) { // Convert to sRGB/P3 using embedded profile try { @@ -329,7 +330,7 @@ class PipelineWorker : public Napi::AsyncWorker { ->set("depth", image.interpretation() == VIPS_INTERPRETATION_RGB16 ? 16 : 8) ->set("intent", VIPS_INTENT_PERCEPTUAL)); } catch(...) { - // Ignore failure of embedded profile + sharp::VipsWarningCallback(nullptr, G_LOG_LEVEL_WARNING, "Invalid embedded profile", nullptr); } } else if (image.interpretation() == VIPS_INTERPRETATION_CMYK) { image = image.icc_transform(processingProfile, VImage::option() diff --git a/test/unit/io.js b/test/unit/io.js index 57ae935e..73f55d8a 100644 --- a/test/unit/io.js +++ b/test/unit/io.js @@ -652,6 +652,7 @@ describe('Input/output', function () { it('toFormat=JPEG takes precedence over WebP extension', function (done) { const outputWebP = fixtures.path('output.webp'); sharp(fixtures.inputPng) + .resize(8) .jpeg() .toFile(outputWebP, function (err, info) { if (err) throw err; @@ -662,6 +663,7 @@ describe('Input/output', function () { it('toFormat=WebP takes precedence over JPEG extension', function (done) { sharp(fixtures.inputPng) + .resize(8) .webp() .toFile(outputJpg, function (err, info) { if (err) throw err; @@ -697,6 +699,27 @@ describe('Input/output', function () { }); }); + it('can ignore ICC profile', async () => { + const [r1, g1, b1] = await sharp(fixtures.inputJpgWithPortraitExif5, { ignoreIcc: true }) + .extract({ width: 1, height: 1, top: 16, left: 16 }) + .raw() + .toBuffer(); + + const [r2, g2, b2] = await sharp(fixtures.inputJpgWithPortraitExif5, { ignoreIcc: false }) + .extract({ width: 1, height: 1, top: 16, left: 16 }) + .raw() + .toBuffer(); + + assert.deepStrictEqual({ r1, g1, b1, r2, g2, b2 }, { + r1: 60, + r2: 77, + g1: 54, + g2: 69, + b1: 20, + b2: 25 + }); + }); + describe('Switch off safety limits for certain formats', () => { it('Valid', () => { assert.doesNotThrow(() => { @@ -816,6 +839,11 @@ describe('Input/output', function () { sharp({ density: 'zoinks' }); }, /Expected number between 1 and 100000 for density but received zoinks of type string/); }); + it('Invalid ignoreIcc: string', function () { + assert.throws(function () { + sharp({ ignoreIcc: 'zoinks' }); + }, /Expected boolean for ignoreIcc but received zoinks of type string/); + }); it('Setting animated property updates pages property', function () { assert.strictEqual(sharp({ animated: false }).options.input.pages, 1); assert.strictEqual(sharp({ animated: true }).options.input.pages, -1);