lovell / lovell/sharp

Join alpha channel with premultiplied tiff file while keeping ICC profile

Open
#4,565 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

question
Dominant language
JavaScript
Stars
32.7k
Forks
1.4k
Avg merge
1d 14h
Merged PRs (30d)
5

Description

## Question about an existing feature
Combining a premultiplied CMYK TIFF file with an alpha channel while preserving its ICC profile.

### What are you trying to achieve?
First of all, thank you for the great work on this library.

I'm trying to join an alpha channel to an existing CMYK tiff image while retaining the exact CMYK values and the original ICC profile. These are the files I received:
- [`cmyk.tiff`](https://github.com/user-attachments/files/29556721/cmyk.tiff): 4 bands + ICC profile
- [`rgba.png`](https://github.com/user-attachments/assets/7616dbc2-6bea-4a20-9cfd-16c00d677fd5): 3 bands + alpha channel.

I'm trying to add the alpha channel from the PNG to the TIFF file, keeping the CMYK values exactly as-is.

I attempted to use `joinChannel` and keep the profile, but I ran into a few different hurdles:
```js
const alphaData = await sharp('rgba.png').extractChannel('alpha').png().toBuffer();
await sharp('cmyk.tiff')
.joinChannel(alphaData)
.keepIccProfile()
.tiff({ compression: 'lzw' })
.toFile('output.tiff');
console.log((await sharp('output.tiff').metadata()).space); // logs `srgb` instead of `cmyk`
```

```js
const alphaData = await sharp('rgba.png').extractChannel('alpha').png().toBuffer();
await sharp('cmyk.tiff')
.joinChannel(alphaData)
.keepIccProfile()
.toColourspace('cmyk') // Added
.tiff({ compression: 'lzw' })
.toFile('output.tiff');
console.log((await sharp('output.tiff').metadata()).hasProfile); // logs `false` instead of `true`
```

```js
const alphaData = await sharp('rgba.png').extractChannel('alpha').png().toBuffer();
await sharp('cmyk.tiff')
.joinChannel(alphaData)
.keepIccProfile()
.toColourspace('cmyk') // Added
.pipelineColourspace('cmyk') // Added
.tiff({ compression: 'lzw' })
.toFile('output.tiff');
console.log((await sharp('output.tiff').metadata())); // throws `Error: Input image exceeds channel limit`
// running `vipsheader` on the generated file returns `2550x3300 uchar, 8 bands, cmyk, tiffload`
```

The only way I've succeeded is to re-encode using the source profile explicitly:
```js
const alphaData = await sharp('rgba.png').extractChannel('alpha').png().toBuffer();
await sharp('cmyk.tiff')
.joinChannel(alphaData)
.withIccProfile('source-profile.icc')
.tiff({ compression: 'lzw' })
.toFile('output.tiff');
console.log((await sharp('output.tiff').metadata())); // CMYK space with 5 bands and ICC attached
```

This has some drawbacks though:
1. it requires extracting the ICC profile from the source file first
2. it re-encodes the file with the same profile, which feels unnecessary as CMYK values can be used as-is
3. the resulting file still needs to be unpremultiplied as the source TIFF file is premultiplied

For drawback (3), I found a workaround using `libvips` CLI directly:
```sh
vips unpremultiply output.tiff output-unpremultiplied.v
vips cast output-unpremultiplied.v output-unpremultiplied-casted.v uchar
vips tiffsave output-unpremultiplied-casted.v output-unpremultiplied.tiff --compression=lzw
```

The result `output-unpremultiplied.tiff` is exactly what I need. Unfortunately, since pipes can't stream these operations in the CLI (https://github.com/libvips/libvips/discussions/4256#discussioncomment-11244848), this approach generates intermediate files exceeding 160MB. Chaining operations using a library seems the recommended approach here.

My questions:
1. Is there a clean way to `joinChannel` an alpha channel while preserving the ICC profile _without_ having to re-encode using `withIccProfile`?
2. What's the feasibility of providing a `unpremultiply` operation in `sharp`? (I've written a PR here previously, so I'd be happy to look into this if it's a welcome addition, but I suspect this might be challenging). Alternatively, would resolving https://github.com/lovell/sharp/issues/4521 be the better approach to handle this entirely with `sharp`? If I understand correctly, it would let me flag the raw image data as already premultiplied, though I'm unsure if that would prevent me from needing to unpremultiply after merging the alpha channel.

Thank you for your time and guidance.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Reproduce the documented joinChannel, keepIccProfile, toColourspace, and pipelineColourspace cases with the supplied CMYK TIFF and RGBA PNG. Compare the output metadata and the libvips CLI unpremultiply workflow, then review issue #4521 and the existing sharp API for the smallest supported path. Done means the intended operation and scope are confirmed, or the request is narrowed to a concrete change.

Written by the indexing model from the issue text.

Assessment

Tech stack
javascript, nodejs
Domain
computer-graphics
Issue type
Feature
Difficulty
4/5
Estimated time
3-5 days
Activity status
Quiet
Clarity
Mostly clear
Newbie friendliness
42/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.