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

Palette-based PNG output, I have to run the process several times before perfect minification #4228

Closed dirago closed 1 month ago

dirago commented 1 month ago

What are you trying to achieve?

hey there, I just wrote a script using sharp for minifying png assets for a vue3 project. Everything works great but if I have to launch the script multiple times for having a "perfect minification" (meaning sharp can no longer reduce file size)

I would like the file size reduction to be done in one step, is it possible?

When you searched for similar issues, what did you find that might be related?

I didn't find anything about this, but maybe I'm doing something wrong?

Please provide a minimal, standalone code sample, without other dependencies, that demonstrates this question

here's my script:

/* eslint-disable no-console */
import chalk from 'chalk';
import fs from 'fs';
import glob from 'glob';
import path from 'path';
import sharp from 'sharp';

const imageExtensions = ['jpg', 'jpeg', 'png', 'gif'];

const minifyImage = async (filePath) => {
  const ext = path.extname(filePath).toLowerCase().replace('.', '');
  const fileName = path.basename(filePath);

  if (imageExtensions.includes(ext)) {
    try {
      const originalSize = fs.statSync(filePath).size;
      let transformer = sharp(filePath);

      if (ext === 'jpg' || ext === 'jpeg') {
        transformer = transformer.jpeg();
      } else if (ext === 'png') {
        transformer = transformer.png({ palette: true });
      } else if (ext === 'gif') {
        transformer = transformer.gif();
      }

      transformer.toBuffer((err, buffer) => {
        if (err) {
          console.log(chalk.red(`Error minifying ${fileName}:`, err));
          return;
        }

        const minifiedSize = buffer.length;

        if (minifiedSize < originalSize) {
          fs.writeFile(filePath, buffer, (writeErr) => {
            if (writeErr) {
              console.log(chalk.red(`Error writing ${fileName}:`, writeErr));
              return;
            }
            console.log(
              `${chalk.greenBright('✓')} ${chalk.bold(fileName)} ${chalk.whiteBright.bgGreen(`${originalSize}kb -> ${minifiedSize}kb`)}`,
            );
          });
        } else {
          console.log(
            `${chalk.whiteBright('✓')} ${chalk.bold(fileName)}️ ${chalk.black.bgWhiteBright('already optimized')}`,
          );
        }
      });
    } catch (error) {
      console.log(chalk.red(`Error minifying ${fileName}:`, error));
    }
  }
};

const runImageMinification = () => {
  glob('src/**/*.{jpg,jpeg,png,gif}', (err, files) => {
    if (err) {
      console.log(chalk.red('Error searching for images :', err));
      return;
    }

    console.log(chalk.cyanBright(`Minification process started for ${files.length} files...`));
    files.forEach((filePath) => minifyImage(filePath));
  });
};

runImageMinification();

Please provide sample image(s) that help explain this question

Capture d’écran 2024-10-03 à 16 15 14

lovell commented 1 month ago

Palette-based PNG output is a lossy process, so further decode/encode roundtrips will throw away more data each time. You could try reducing the quality setting from its default of 100 if you want palette-based output and value smaller file size over "quality". You'll need to experiment with a suitable value for your given scenario.

-        transformer = transformer.png({ palette: true });
+        transformer = transformer.png({ quality: 50 });

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

dirago commented 1 month ago

Thanks for your answer @lovell, I played a lot with the given options, especially with quality but I was not able to get a conclusive result