diff --git a/docs/api-output.md b/docs/api-output.md index a67cf16b..3fa0dc21 100644 --- a/docs/api-output.md +++ b/docs/api-output.md @@ -91,7 +91,8 @@ Returns **[Promise][5]<[Buffer][8]>** when no callback is provided ## withMetadata Include all metadata (EXIF, XMP, IPTC) from the input image in the output image. -This will also convert to and add a web-friendly sRGB ICC profile. +This will also convert to and add a web-friendly sRGB ICC profile unless a custom +output profile is provided. The default behaviour, when `withMetadata` is not used, is to convert to the device-independent sRGB colour space and strip all metadata, including the removal of any ICC profile. @@ -100,6 +101,7 @@ sRGB colour space and strip all metadata, including the removal of any ICC profi - `options` **[Object][6]?** - `options.orientation` **[number][9]?** value between 1 and 8, used to update the EXIF `Orientation` tag. + - `options.icc` **[string][2]?** filesystem path to output ICC profile, defaults to sRGB. ### Examples diff --git a/docs/changelog.md b/docs/changelog.md index 3e9404f7..6592bb4c 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -27,6 +27,10 @@ Requires libvips v8.10.0 [#2259](https://github.com/lovell/sharp/pull/2259) [@vouillon](https://github.com/vouillon) +* Add support to `withMetadata` for custom ICC profile. + [#2271](https://github.com/lovell/sharp/pull/2271) + [@roborourke](https://github.com/roborourke) + * Ensure prebuilt binaries for ARM default to v7 when using Electron. [#2292](https://github.com/lovell/sharp/pull/2292) [@diegodev3](https://github.com/diegodev3) diff --git a/docs/humans.txt b/docs/humans.txt index f9d39427..9b99efe7 100644 --- a/docs/humans.txt +++ b/docs/humans.txt @@ -194,3 +194,6 @@ GitHub: https://github.com/vouillon Name: Tomáš Szabo GitHub: https://github.com/deftomat + +Name: Robert O'Rourke +GitHub: https://github.com/roborourke diff --git a/lib/constructor.js b/lib/constructor.js index ee941b74..eb89e455 100644 --- a/lib/constructor.js +++ b/lib/constructor.js @@ -192,6 +192,7 @@ const Sharp = function (input, options) { streamOut: false, withMetadata: false, withMetadataOrientation: -1, + withMetadataIcc: '', resolveWithObject: false, // output format jpegQuality: 80, diff --git a/lib/output.js b/lib/output.js index 047876f8..13d349e0 100644 --- a/lib/output.js +++ b/lib/output.js @@ -119,7 +119,8 @@ function toBuffer (options, callback) { /** * Include all metadata (EXIF, XMP, IPTC) from the input image in the output image. - * This will also convert to and add a web-friendly sRGB ICC profile. + * This will also convert to and add a web-friendly sRGB ICC profile unless a custom + * output profile is provided. * * The default behaviour, when `withMetadata` is not used, is to convert to the device-independent * sRGB colour space and strip all metadata, including the removal of any ICC profile. @@ -132,6 +133,7 @@ function toBuffer (options, callback) { * * @param {Object} [options] * @param {number} [options.orientation] value between 1 and 8, used to update the EXIF `Orientation` tag. + * @param {string} [options.icc] filesystem path to output ICC profile, defaults to sRGB. * @returns {Sharp} * @throws {Error} Invalid parameters */ @@ -145,6 +147,13 @@ function withMetadata (options) { throw is.invalidParameterError('orientation', 'integer between 1 and 8', options.orientation); } } + if (is.defined(options.icc)) { + if (is.string(options.icc)) { + this.options.withMetadataIcc = options.icc; + } else { + throw is.invalidParameterError('icc', 'string filesystem path to ICC profile', options.icc); + } + } } return this; } diff --git a/package.json b/package.json index efa40f8f..debf207e 100644 --- a/package.json +++ b/package.json @@ -68,7 +68,8 @@ "Brychan Bennett-Odlum ", "Edward Silverton ", "Roman Malieiev ", - "Tomas Szabo " + "Tomas Szabo ", + "Robert O'Rourke " ], "scripts": { "install": "(node install/libvips && node install/dll-copy && prebuild-install) || (node-gyp rebuild && node install/dll-copy)", diff --git a/src/pipeline.cc b/src/pipeline.cc index 21ff0063..2163b55f 100644 --- a/src/pipeline.cc +++ b/src/pipeline.cc @@ -684,6 +684,15 @@ class PipelineWorker : public Napi::AsyncWorker { } } + // Apply output ICC profile + if (!baton->withMetadataIcc.empty()) { + image = image.icc_transform( + const_cast(baton->withMetadataIcc.data()), + VImage::option() + ->set("input_profile", "srgb") + ->set("intent", VIPS_INTENT_PERCEPTUAL)); + } + // Override EXIF Orientation tag if (baton->withMetadata && baton->withMetadataOrientation != -1) { image = sharp::SetExifOrientation(image, baton->withMetadataOrientation); @@ -1319,6 +1328,7 @@ Napi::Value pipeline(const Napi::CallbackInfo& info) { baton->fileOut = sharp::AttrAsStr(options, "fileOut"); baton->withMetadata = sharp::AttrAsBool(options, "withMetadata"); baton->withMetadataOrientation = sharp::AttrAsUint32(options, "withMetadataOrientation"); + baton->withMetadataIcc = sharp::AttrAsStr(options, "withMetadataIcc"); // Format-specific baton->jpegQuality = sharp::AttrAsUint32(options, "jpegQuality"); baton->jpegProgressive = sharp::AttrAsBool(options, "jpegProgressive"); diff --git a/src/pipeline.h b/src/pipeline.h index 650e5f58..ef2d668d 100644 --- a/src/pipeline.h +++ b/src/pipeline.h @@ -155,6 +155,7 @@ struct PipelineBaton { std::string err; bool withMetadata; int withMetadataOrientation; + std::string withMetadataIcc; std::unique_ptr convKernel; int convKernelWidth; int convKernelHeight; diff --git a/test/fixtures/expected/hilutite.jpg b/test/fixtures/expected/hilutite.jpg new file mode 100644 index 00000000..f889bc5c Binary files /dev/null and b/test/fixtures/expected/hilutite.jpg differ diff --git a/test/fixtures/expected/icc-cmyk.jpg b/test/fixtures/expected/icc-cmyk.jpg new file mode 100644 index 00000000..a107c28d Binary files /dev/null and b/test/fixtures/expected/icc-cmyk.jpg differ diff --git a/test/fixtures/hilutite.icm b/test/fixtures/hilutite.icm new file mode 100644 index 00000000..6a7ad4be Binary files /dev/null and b/test/fixtures/hilutite.icm differ diff --git a/test/unit/metadata.js b/test/unit/metadata.js index b34cceef..ee94386a 100644 --- a/test/unit/metadata.js +++ b/test/unit/metadata.js @@ -501,6 +501,42 @@ describe('Image metadata', function () { }); }); + it('Apply CMYK output ICC profile', function (done) { + const output = fixtures.path('output.icc-cmyk.jpg'); + sharp(fixtures.inputJpg) + .withMetadata({ icc: 'cmyk' }) + .toFile(output, function (err, info) { + if (err) throw err; + sharp(output).metadata(function (err, metadata) { + if (err) throw err; + assert.strictEqual(true, metadata.hasProfile); + assert.strictEqual('cmyk', metadata.space); + assert.strictEqual(4, metadata.channels); + // ICC + assert.strictEqual('object', typeof metadata.icc); + assert.strictEqual(true, metadata.icc instanceof Buffer); + const profile = icc.parse(metadata.icc); + assert.strictEqual('object', typeof profile); + assert.strictEqual('CMYK', profile.colorSpace); + assert.strictEqual('Relative', profile.intent); + assert.strictEqual('Printer', profile.deviceClass); + }); + fixtures.assertSimilar(output, fixtures.path('expected/icc-cmyk.jpg'), { threshold: 0 }, done); + }); + }); + + it('Apply custom output ICC profile', function (done) { + const output = fixtures.path('output.hilutite.jpg'); + sharp(fixtures.inputJpg) + .withMetadata({ icc: fixtures.path('hilutite.icm') }) + .toFile(output, function (err, info) { + if (err) throw err; + fixtures.assertMaxColourDistance(output, fixtures.path('expected/hilutite.jpg'), 0); + fixtures.assertMaxColourDistance(output, fixtures.inputJpg, 16.5); + done(); + }); + }); + it('Include metadata in output, enabled via empty object', () => sharp(fixtures.inputJpgWithExif) .withMetadata({}) @@ -675,5 +711,10 @@ describe('Image metadata', function () { sharp().withMetadata({ orientation: 9 }); }); }); + it('Non string icc', function () { + assert.throws(function () { + sharp().withMetadata({ icc: true }); + }); + }); }); });