Simplify ICC processing when retaining input profiles #4468

Takes advantage of libvips' improved ICC handling
This commit is contained in:
Lovell Fuller 2025-11-04 15:06:49 +00:00
parent 4f9f8179a6
commit e1628d8ef5
No known key found for this signature in database
GPG Key ID: 26C1635893ACB81F
5 changed files with 38 additions and 17 deletions

View File

@ -201,7 +201,7 @@ const dataWithMergedExif = await sharp(inputWithExif)
Keep ICC profile from the input image in the output image. Keep ICC profile from the input image in the output image.
For non-RGB output use [toColourspace](/api-colour/#tocolourspace). When input and output colour spaces differ, use with [toColourspace](/api-colour/#tocolourspace) and optionally [pipelineColourspace](/api-colour/#pipelinecolourspace).
**Since**: 0.33.0 **Since**: 0.33.0
@ -211,6 +211,14 @@ const outputWithIccProfile = await sharp(inputWithIccProfile)
.keepIccProfile() .keepIccProfile()
.toBuffer(); .toBuffer();
``` ```
**Example**
```js
const cmykOutputWithIccProfile = await sharp(cmykInputWithIccProfile)
.pipelineColourspace('cmyk')
.toColourspace('cmyk')
.keepIccProfile()
.toBuffer();
```
## withIccProfile ## withIccProfile

View File

@ -16,3 +16,6 @@ slug: changelog/v0.34.5
* Improve error messaging when only warnings issued. * Improve error messaging when only warnings issued.
[#4465](https://github.com/lovell/sharp/issues/4465) [#4465](https://github.com/lovell/sharp/issues/4465)
* Simplify ICC processing when retaining input profiles.
[#4468](https://github.com/lovell/sharp/issues/4468)

View File

@ -256,7 +256,7 @@ function withExifMerge (exif) {
/** /**
* Keep ICC profile from the input image in the output image. * Keep ICC profile from the input image in the output image.
* *
* For non-RGB output use {@link /api-colour/#tocolourspace toColourspace}. * When input and output colour spaces differ, use with {@link /api-colour/#tocolourspace toColourspace} and optionally {@link /api-colour/#pipelinecolourspace pipelineColourspace}.
* *
* @since 0.33.0 * @since 0.33.0
* *
@ -265,6 +265,13 @@ function withExifMerge (exif) {
* .keepIccProfile() * .keepIccProfile()
* .toBuffer(); * .toBuffer();
* *
* @example
* const cmykOutputWithIccProfile = await sharp(cmykInputWithIccProfile)
* .pipelineColourspace('cmyk')
* .toColourspace('cmyk')
* .keepIccProfile()
* .toBuffer();
*
* @returns {Sharp} * @returns {Sharp}
*/ */
function keepIccProfile () { function keepIccProfile () {

View File

@ -797,20 +797,14 @@ class PipelineWorker : public Napi::AsyncWorker {
image = sharp::EnsureAlpha(image, baton->ensureAlpha); image = sharp::EnsureAlpha(image, baton->ensureAlpha);
} }
// Convert image to sRGB, if not already // Ensure output colour space
if (sharp::Is16Bit(image.interpretation())) { if (sharp::Is16Bit(image.interpretation())) {
image = image.cast(VIPS_FORMAT_USHORT); image = image.cast(VIPS_FORMAT_USHORT);
} }
if (image.interpretation() != baton->colourspace) { if (image.interpretation() != baton->colourspace) {
// 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())); image = image.colourspace(baton->colourspace, VImage::option()->set("source_space", image.interpretation()));
// Transform colours from embedded profile to output profile if (inputProfile.first && baton->withIccProfile.empty()) {
if ((baton->keepMetadata & VIPS_FOREIGN_KEEP_ICC) && baton->colourspacePipeline != VIPS_INTERPRETATION_CMYK && image = sharp::SetProfile(image, inputProfile);
baton->withIccProfile.empty() && sharp::HasProfile(image)) {
image = image.icc_transform(processingProfile, VImage::option()
->set("embedded", true)
->set("depth", sharp::Is16Bit(image.interpretation()) ? 16 : 8)
->set("intent", VIPS_INTENT_PERCEPTUAL));
} }
} }
@ -845,8 +839,6 @@ class PipelineWorker : public Napi::AsyncWorker {
} catch(...) { } catch(...) {
sharp::VipsWarningCallback(nullptr, G_LOG_LEVEL_WARNING, "Invalid profile", nullptr); sharp::VipsWarningCallback(nullptr, G_LOG_LEVEL_WARNING, "Invalid profile", nullptr);
} }
} else if (baton->keepMetadata & VIPS_FOREIGN_KEEP_ICC) {
image = sharp::SetProfile(image, inputProfile);
} }
// Negate the colours in the image // Negate the colours in the image

View File

@ -651,16 +651,27 @@ describe('Image metadata', () => {
assert.strictEqual(description, 'Generic RGB Profile'); assert.strictEqual(description, 'Generic RGB Profile');
}); });
it('keep existing ICC profile, ignore colourspace conversion', async () => { it('keep existing CMYK input profile for CMYK output', async () => {
const data = await sharp(fixtures.inputJpgWithCmykProfile)
.keepIccProfile()
.toColourspace('cmyk')
.toBuffer();
const metadata = await sharp(data).metadata();
assert.strictEqual(metadata.channels, 4);
const { description } = icc.parse(metadata.icc);
assert.strictEqual(description, 'U.S. Web Coated (SWOP) v2');
});
it('transform with but discard existing RGB input profile for CMYK output', async () => {
const data = await sharp(fixtures.inputJpgWithExif) const data = await sharp(fixtures.inputJpgWithExif)
.keepIccProfile() .keepIccProfile()
.toColourspace('cmyk') .toColourspace('cmyk')
.toBuffer(); .toBuffer();
const metadata = await sharp(data).metadata(); const metadata = await sharp(data).metadata();
assert.strictEqual(metadata.channels, 3); assert.strictEqual(metadata.channels, 4);
const { description } = icc.parse(metadata.icc); assert.strictEqual(metadata.icc, undefined);
assert.strictEqual(description, 'Generic RGB Profile');
}); });
it('keep existing ICC profile, avoid colour transform', async () => { it('keep existing ICC profile, avoid colour transform', async () => {