sharp icon indicating copy to clipboard operation
sharp copied to clipboard

Channel excluded from image manipulations, if the channel was `join`ed at later time

Open SuggonM opened this issue 1 year ago • 4 comments

I'm not quite sure if the title is accurate to Sharp's internal algorithm, but joinChannel does appear to exhibit an unusual behavior at least somewhere.

As a very simple example to reproduce, I'm extracting all 4 channels as raw from a colorful PNG, then joining those channels into a new instance of image. The expected result should be 1:1 duplicate of the original.

import sharp from 'sharp';

const orig = sharp('./colors.png');

// decompose into individual RGBA channel values
// concept taken from https://github.com/lovell/sharp/issues/1757#issuecomment-503653509
const channels = ['red', 'green', 'blue', 'alpha'];
const decompose = channels.map(
	channel => orig.extractChannel(channel).raw().toBuffer()
);
const [r, g, b, a] = await Promise.all(decompose);

// copy dimensions from original
const { info } = await orig.toBuffer({ resolveWithObject: true });
const metadata = { raw: {
	...info,
	channels: 1,
	background: 'transparent'
}};

// join/compose all 4 decomposed channels to create an original-alike
const dupe = sharp(r, metadata).joinChannel([g, b, a], metadata);

// export
dupe.toFile('./colors-dupe.png');

Things seem to appear nice and fine until this point - the export is still 1:1 with the original. However, any further manipulation on the dupe from here on, will result in only the red channel being affected. As an example, rotating by 90 degrees:

// export
dupe.rotate(90).toFile('./colors-dupe.png');

This is my exact concern, as one would normally expect all 4 channels being manipulated as a whole.

In order to verify it's only the red channel being manipulated:

  1. Open colors-dupe.png in GIMP.
  2. Go to Channels panel (next to Layers panel).
  3. Hide all 3 channels excluding the red.
  4. Result: Only the red channel appears rotated by 90 degrees.

SuggonM avatar Aug 20 '24 04:08 SuggonM

Please try something like the following:

// export
- dupe.toFile('./colors-dupe.png');
+ await dupe.toFile('./colors-dupe.png');
// export
- dupe.rotate(90).toFile('./colors-dupe.png');
+ await sharp('./colors-dupe.png').rotate(90).toFile('./colors-dupe-rotated.png');

lovell avatar Aug 20 '24 10:08 lovell

The said change works 👍

SuggonM avatar Aug 21 '24 05:08 SuggonM

Although, I can't really say I'm supportive with the idea of having to spawn an extra image and create another sharp instance for it.

Does this mean achieving it is impossible without first exporting into an intermediate file?

(more context would be welcome!)

SuggonM avatar Aug 21 '24 05:08 SuggonM

And if not that then, by extension, would it be possible to somewhat work this around using the composite method? Not trying to be troublesome or anything, just looking for different perspectives to achieve things 🙂

SuggonM avatar Aug 21 '24 11:08 SuggonM