Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
96 changes: 63 additions & 33 deletions optimize.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { spawn } from 'node:child_process';
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';

import guetzli from '@343dev/guetzli';
import execBuffer from 'exec-buffer';
import gifsicle from 'gifsicle';
import pLimit from 'p-limit';
import sharp from 'sharp';
Expand Down Expand Up @@ -164,28 +164,30 @@ async function processJpeg({ fileBuffer, config, isLossless }) {
const sharpImage = sharp(fileBuffer)
.rotate(); // Rotate image using information from EXIF Orientation tag

if (isLossless) {
const inputBuffer = await sharpImage
.toColorspace('srgb') // Replace colorspace (guetzli works only with sRGB)
.jpeg({ quality: 100, optimizeCoding: false }) // Applying maximum quality to minimize losses during image processing with sharp
if (!isLossless) {
return sharpImage
.jpeg(config?.jpeg?.lossy || {})
.toBuffer();

return execBuffer({
bin: guetzli,
args: [
...optionsToArguments({
options: config?.jpeg?.lossless || {},
}),
execBuffer.input,
execBuffer.output,
],
input: inputBuffer,
});
}

return sharpImage
.jpeg(config?.jpeg?.lossy || {})
const inputBuffer = await sharpImage
.toColorspace('srgb') // Replace colorspace (guetzli works only with sRGB)
.jpeg({ quality: 100, optimizeCoding: false }) // Applying maximum quality to minimize losses during image processing with sharp
.toBuffer();

const commandOptions = [
...optionsToArguments({
options: config?.jpeg?.lossless || {},
}),
'-',
'-',
];

return pipe({
command: guetzli,
commandOptions,
inputBuffer,
});
}

function processPng({ fileBuffer, config, isLossless }) {
Expand All @@ -195,20 +197,20 @@ function processPng({ fileBuffer, config, isLossless }) {
}

function processGif({ fileBuffer, config, isLossless }) {
return execBuffer({
bin: gifsicle,
args: [
...optionsToArguments({
options: (isLossless ? config?.gif?.lossless : config?.gif?.lossy) || {},
concat: true,
}),
`--threads=${os.cpus().length}`,
'--no-warnings',
'--output',
execBuffer.output,
execBuffer.input,
],
input: fileBuffer,
const commandOptions = [
...optionsToArguments({
options: (isLossless ? config?.gif?.lossless : config?.gif?.lossy) || {},
concat: true,
}),
`--threads=${os.cpus().length}`,
'--no-warnings',
'-',
];

return pipe({
command: gifsicle,
commandOptions,
inputBuffer: fileBuffer,
});
}

Expand All @@ -220,3 +222,31 @@ function processSvg({ fileBuffer, config }) {
).data,
);
}

function pipe({ command, commandOptions, inputBuffer }) {
return new Promise((resolve, reject) => {
const process = spawn(command, commandOptions);

process.stdin.write(inputBuffer);
process.stdin.end();

const stdoutChunks = [];
process.stdout.on('data', chunk => {
stdoutChunks.push(chunk);
});

process.on('error', error => {
reject(new Error(`Error processing image: ${error.message}`));
});

process.on('close', code => {
if (code !== 0) {
reject(new Error(`Image optimization process exited with code ${code}`));
return;
}

const processedFileBuffer = Buffer.concat(stdoutChunks);
resolve(processedFileBuffer);
});
});
}
46 changes: 12 additions & 34 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 1 addition & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,10 +45,9 @@
"node": ">=18.18"
},
"dependencies": {
"@343dev/guetzli": "^1.0.1",
"@343dev/guetzli": "^1.1.0",
"cli-progress": "^3.11.0",
"commander": "^12.1.0",
"exec-buffer": "^3.2.0",
"fdir": "^6.4.0",
"gifsicle": "^7.0.1",
"p-limit": "^6.1.0",
Expand Down
2 changes: 1 addition & 1 deletion tests/cli.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ describe('CLI', () => {
const stdout = runCliWithParameters(`--lossless ${workDirectory}${file}`);

expectFileRatio({
stdout, file, maxRatio: 50, minRatio: 45,
stdout, file, maxRatio: 55, minRatio: 45,
});
});

Expand Down