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.27k stars 1.3k forks source link

Convert an image to use only the colors from a palette. #3724

Closed infacto closed 1 year ago

infacto commented 1 year ago

Feature request

Convert an image to use only the colors from a palette. Optional with dithering (Floyd–Steinberg). Like with the program Aseprite or Photoshop. Aka indexed color. The colors from the input image are automatically converted to a specific palette that is closest to the color. The color palette could be imported from a png or known color palette file ext.

lovell commented 1 year ago

GIF and palette-based PNG output already does this for you.

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

infacto commented 1 year ago

Thanks, but can I use an external palette? If I understand correctly, you can only reduce the color amount used in png from its own content. But I want to use the color palette from another file.

I already tried to use a png file with all the colors I want and enabled palette option. Then used toBuffer to use again in sharp with another file with composite ('over' to replace the content). But it ignores the color palette. I would expect to only use the available colors from the palette.

lovell commented 1 year ago

There's a work-in-progress PR for this at https://github.com/libvips/libvips/pull/3122

The most appropriate next step for you would be to take what's there so far and implement it fully, if you're able.

infacto commented 1 year ago

Okay great, looks promising. Currently I crafted a script in JS to iterate through the image and find the closest color from a color palette array.

const colorPalette = []; // [r, g, b][]
const image = await sharp('input.png').raw().toBuffer();

// Expecting 3 channels (r, g, b without alpha).
for (let i = 0; i < image.length; i += 3) {
  const r = image[i + 0];
  const g = image[i + 1];
  const b = image[i + 2];

  let closestColor = colorPalette[0];
  let minDistance = Number.MAX_SAFE_INTEGER;

  for (const color of colorPalette) {
    const distance = Math.sqrt(
      Math.pow(r - color[0], 2) +
      Math.pow(g - color[1], 2) +
      Math.pow(b - color[2], 2));

    if (distance < minDistance) {
      minDistance = distance;
      closestColor = color;
    }
  }

  image[i + 0] = closestColor[0];
  image[i + 1] = closestColor[1];
  image[i + 2] = closestColor[2];
}

sharp(image, { raw: { width: 64, height: 64, channels: 3 } }).toFile('output.png');

You could also use metadata from sharp to get width, height and channels.

It would be great if sharp supports a better way to iterate and manipulate per pixel. Something like with Jimp.scan. And support format bmp would be also great. I use the package sharp-bmp.