diff --git a/docs/changelog.md b/docs/changelog.md index 6f31f21b..db0165b0 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -12,6 +12,9 @@ Requires libvips v8.11.3 * Ensure background is always premultiplied when compositing. [#2858](https://github.com/lovell/sharp/issues/2858) +* Ensure images with P3 profiles retain full gamut. + [#2862](https://github.com/lovell/sharp/issues/2862) + ### v0.29.0 - 17th August 2021 * Drop support for Node.js 10, now requires Node.js >= 12.13.0. diff --git a/src/pipeline.cc b/src/pipeline.cc index 7a154d76..149a6e01 100644 --- a/src/pipeline.cc +++ b/src/pipeline.cc @@ -291,14 +291,15 @@ class PipelineWorker : public Napi::AsyncWorker { } // Ensure we're using a device-independent colour space + char const *processingProfile = image.interpretation() == VIPS_INTERPRETATION_RGB16 ? "p3" : "srgb"; if ( sharp::HasProfile(image) && image.interpretation() != VIPS_INTERPRETATION_LABS && image.interpretation() != VIPS_INTERPRETATION_GREY16 ) { - // Convert to sRGB using embedded profile + // Convert to sRGB/P3 using embedded profile try { - image = image.icc_transform("srgb", VImage::option() + image = image.icc_transform(processingProfile, VImage::option() ->set("embedded", TRUE) ->set("depth", image.interpretation() == VIPS_INTERPRETATION_RGB16 ? 16 : 8) ->set("intent", VIPS_INTENT_PERCEPTUAL)); @@ -306,7 +307,7 @@ class PipelineWorker : public Napi::AsyncWorker { // Ignore failure of embedded profile } } else if (image.interpretation() == VIPS_INTERPRETATION_CMYK) { - image = image.icc_transform("srgb", VImage::option() + image = image.icc_transform(processingProfile, VImage::option() ->set("input_profile", "cmyk") ->set("intent", VIPS_INTENT_PERCEPTUAL)); } @@ -715,9 +716,10 @@ class PipelineWorker : public Napi::AsyncWorker { // Convert colourspace, pass the current known interpretation so libvips doesn't have to guess image = image.colourspace(baton->colourspace, VImage::option()->set("source_space", image.interpretation())); // Transform colours from embedded profile to output profile - if (baton->withMetadata && sharp::HasProfile(image)) { - image = image.icc_transform(vips_enum_nick(VIPS_TYPE_INTERPRETATION, baton->colourspace), - VImage::option()->set("embedded", TRUE)); + if (baton->withMetadata && sharp::HasProfile(image) && baton->withMetadataIcc.empty()) { + image = image.icc_transform("srgb", VImage::option() + ->set("embedded", TRUE) + ->set("intent", VIPS_INTENT_PERCEPTUAL)); } } @@ -726,7 +728,8 @@ class PipelineWorker : public Napi::AsyncWorker { image = image.icc_transform( const_cast(baton->withMetadataIcc.data()), VImage::option() - ->set("input_profile", "srgb") + ->set("input_profile", processingProfile) + ->set("embedded", TRUE) ->set("intent", VIPS_INTENT_PERCEPTUAL)); } // Override EXIF Orientation tag diff --git a/test/fixtures/index.js b/test/fixtures/index.js index 505f8ca8..9e8b019a 100644 --- a/test/fixtures/index.js +++ b/test/fixtures/index.js @@ -92,6 +92,7 @@ module.exports = { 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 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 diff --git a/test/fixtures/p3.png b/test/fixtures/p3.png new file mode 100644 index 00000000..96e10ff1 Binary files /dev/null and b/test/fixtures/p3.png differ diff --git a/test/unit/colourspace.js b/test/unit/colourspace.js index 875f9675..7853125a 100644 --- a/test/unit/colourspace.js +++ b/test/unit/colourspace.js @@ -105,6 +105,25 @@ describe('Colour space conversion', function () { }); }); + it('Convert P3 to sRGB', async () => { + const [r, g, b] = await sharp(fixtures.inputPngP3) + .raw() + .toBuffer(); + assert.strictEqual(r, 255); + assert.strictEqual(g, 0); + assert.strictEqual(b, 0); + }); + + it('Passthrough P3', async () => { + const [r, g, b] = await sharp(fixtures.inputPngP3) + .withMetadata({ icc: 'p3' }) + .raw() + .toBuffer(); + assert.strictEqual(r, 234); + assert.strictEqual(g, 51); + assert.strictEqual(b, 34); + }); + it('Invalid pipelineColourspace input', function () { assert.throws(function () { sharp(fixtures.inputJpg)