lovell / sharp

High performance Node.js image processing, the fastest module to resize JPEG, PNG, WebP, AVIF and TIFF images. Uses the libvips library.
https://sharp.pixelplumbing.com
Apache License 2.0
29.14k stars 1.3k forks source link

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

Open SuggonM opened 2 months ago

SuggonM commented 2 months ago

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.
lovell commented 2 months ago

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');
SuggonM commented 2 months ago

The said change works 👍

SuggonM commented 2 months ago

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 commented 2 months ago

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 🙂