sharp icon indicating copy to clipboard operation
sharp copied to clipboard

premultiplied Usage: When compress png with premulitiplied, then output png shows bad.

Open tangkaikk opened this issue 8 months ago • 12 comments

Possible bug

// Copyright 2013 Lovell Fuller and others.
// SPDX-License-Identifier: Apache-2.0

'use strict';

const sharp = require('sharp');
sharp('PariteShip.png',{ premultiplied: true })
  .png({ quality: 80},{ premultiplied: true })
  .toFile('compressed.png',{ premultiplied: true })
  

What is the expected behaviour?

Image

If I delete the compress code, then output png shows good. below is the correct result Image

tangkaikk avatar Jun 12 '25 09:06 tangkaikk

Is this LLM-generated code? None of these functions accept premultiplied as an option.

When you provide a quality option to png() you are selecting to use lossy, quantised, palette-based output.

https://sharp.pixelplumbing.com/api-output/#png

lovell avatar Jun 12 '25 09:06 lovell

Is this LLM-generated code? None of these functions accept premultiplied as an option.

When you provide a quality option to png() you are selecting to use lossy, quantised, palette-based output.

https://sharp.pixelplumbing.com/api-output/#png

I wrote it , just for test the premultiplied usage. Is that possible to compress the png to support premultiplied ? Instead I use the tinypng to compress the png, the output png shows good.

tangkaikk avatar Jun 12 '25 10:06 tangkaikk

I think the best thing for you to do is create a complete, minimal, standalone repo with code and image(s) that allows someone else to reproduce and therefore understand what you're looking for.

lovell avatar Jun 12 '25 22:06 lovell

@tangkaikk Were you able to make any progress with this? If you still require help, please can you provide the requested info.

lovell avatar Jul 07 '25 11:07 lovell

pic-compressShowBad.zip Here is the minimal repo for you. There are two pics under folder pic-compressShowBad\assets\main\native\4a, the one named "4a28a44c-8feb-4ca5-9913-9dd8d2b023c7" shows bad which compressed by sharp, the other one "PariteShip-goodPic" shows good which is the original picture.

The compress code is below:

const sharp = require('sharp');
sharp('PariteShip.png')
  .png({ quality: 80})
  .toFile('compressed.png')

tangkaikk avatar Jul 16 '25 07:07 tangkaikk

Thanks, as I mentioned above, when you provide a quality option to png() you are selecting to use lossy, quantised, palette-based output.

https://sharp.pixelplumbing.com/api-output/#png

The sample image provided is already a palette-based image that has the maximum 256 palette entries (768 bytes is 256 RGB entries):

$ pngchunks PariteShip-goodPic.png | grep PLTE
Chunk: Data Length 768 (max 2147483647), Type 1163152464 [PLTE]

By selecting a quality of 80 you are reducing the palette size (474 bytes is 158 RGB entries):

$ pngchunks 4a28a44c-8feb-4ca5-9913-9dd8d2b023c7.png | grep PLTE
Chunk: Data Length 474 (max 2147483647), Type 1163152464 [PLTE]

I would suggest you use palette: true and do not provide a quality:

-   .png({ quality: 80 })
+   .png({ palette: true })

lovell avatar Jul 16 '25 09:07 lovell

Image

Use the suggestion, the problem still exist.

tangkaikk avatar Jul 16 '25 10:07 tangkaikk

This looks like an artefact of the quantisation process used to reduce the number of colours in an image, hence my original comment about palette size.

You might have some luck removing the use of dithering by setting dither to zero.

.png({ palette: true, dither: 0 })

If this doesn't produce suitable output, I'd probably avoid palette-based PNG entirely given quality appears to be of your greatest concern.

lovell avatar Jul 16 '25 10:07 lovell

The output still the same. The original picture has been premultiplied Alpha, how can I keep it? Of course, when I remove premultiplied Alpha, the output turns right. My greatest concern is use the sharp to shrink the picture size meanwhile it can still keep the premultiplied Alpha result.

tangkaikk avatar Jul 17 '25 01:07 tangkaikk

I set the premultiplied option true, but output info it turns false.

Image Image

tangkaikk avatar Jul 17 '25 01:07 tangkaikk

The premultiplied input property you've highlighted is for raw pixel based input, which you do not appear to be using.

The premultiplied output property you've highlighted tells you whether any operations in the pipeline resulted in sharp applying a premultiply/unpremultiply roundtrip whilst processing, as documented by toFile:

info contains the output image format, size (bytes), width, height, channels and premultiplied (indicating if premultiplication was used).

As I mentioned above, I think the best thing for you to do is create a complete, minimal, standalone repo with code and image(s) that allows someone else to reproduce and therefore understand what you're looking for.

lovell avatar Jul 17 '25 12:07 lovell

As I mention before, the original pic is premultiplied one. I use raw.premultiplied option to avoid sharp premultiplying the image, but the output file which I copy to the folder pic-compressShowBad\assets\main\native\4a and renamed is as "4a28a44c-8feb-4ca5-9913-9dd8d2b023c7" shows bad.

Here is the code I use:

const sharp = require('sharp');

async function checkAndCompressPng(inputPath, outputPath) {
  try {
    // Load image and check metadata
	let options =  new Object();
	  options["raw"]= new Object();
	  options["raw"]["premultiplied"] = true;
	  options["raw"]["width"] = 2048;
	  options["raw"]["height"] = 1024;
	  options["raw"]["channels"] = 4;
    const image = sharp(inputPath, options);

    // Compress PNG
    await image
      .png({
        progressive: true,
		  palette: true,
		  quality: 60
      })
	  .keepMetadata()
      .toFile(outputPath,(err,info)=>{
		  console.log("info",info);
		  });
  } catch (err) {
    console.error("Error:", err);
  }
}
checkAndCompressPng('PariteShip.png', 'output.png');

tangkaikk avatar Jul 18 '25 02:07 tangkaikk