pngquant
Version:
The pngquant utility as a readable/writable stream
209 lines (187 loc) • 5.41 kB
JavaScript
import childProcess from 'child_process';
import { Stream } from 'stream';
import util from 'util';
import which from 'which';
import memoizeAsync from 'memoizeasync';
function PngQuant(pngQuantArgs) {
Stream.call(this);
this.pngQuantArgs = pngQuantArgs;
if (!this.pngQuantArgs || this.pngQuantArgs.length === 0) {
this.pngQuantArgs = [256];
}
this.writable = this.readable = true;
this.hasEnded = false;
this.seenDataOnStdout = false;
}
util.inherits(PngQuant, Stream);
PngQuant.getBinaryPath = memoizeAsync((cb) => {
if (PngQuant.binaryPath !== undefined) {
setImmediate(() => {
cb(null, PngQuant.binaryPath);
});
return;
}
which('pngquant', async (err, pngQuantBinaryPath) => {
if (err) {
const pngQuantModule = await import('pngquant-bin');
pngQuantBinaryPath = pngQuantModule.default;
}
if (pngQuantBinaryPath) {
cb(null, pngQuantBinaryPath);
} else {
cb(
new Error(
'No pngquant binary in PATH and pngquant-bin does not provide a pre-built binary for your architecture'
)
);
}
});
});
PngQuant.setBinaryPath = (binaryPath) => {
PngQuant.binaryPath = binaryPath;
};
PngQuant.prototype._error = function (err) {
if (!this.hasEnded) {
this.hasEnded = true;
this.cleanUp();
this.emit('error', err);
}
};
PngQuant.prototype.cleanUp = function () {
if (this.pngQuantProcess) {
this.pngQuantProcess.kill();
this.pngQuantProcess = null;
}
this.bufferedChunks = null;
};
PngQuant.prototype.destroy = function () {
if (!this.hasEnded) {
this.hasEnded = true;
this.cleanUp();
this.bufferedChunks = null;
}
};
PngQuant.prototype.write = function (chunk) {
if (this.hasEnded) {
return;
}
if (!this.pngQuantProcess && !this.bufferedChunks) {
this.bufferedChunks = [];
PngQuant.getBinaryPath((err, pngCrushBinaryPath) => {
if (this.hasEnded) {
return;
}
if (err) {
return this._error(err);
}
this.commandLine =
pngCrushBinaryPath +
(this.pngQuantArgs ? ` ${this.pngQuantArgs.join(' ')}` : ''); // For debugging
this.pngQuantProcess = childProcess.spawn(
pngCrushBinaryPath,
this.pngQuantArgs,
{
windowsHide: true,
}
);
this.pngQuantProcess.once('error', this._error.bind(this));
this.pngQuantProcess.stdin.once('error', this._error.bind(this));
this.pngQuantProcess.stdout.once('error', this._error.bind(this));
this.pngQuantProcess.stderr.on('data', (data) => {
if (!this.hasEnded) {
this._error(
new Error(
`Saw pngquant output on stderr: ${data.toString('ascii')}`
)
);
this.hasEnded = true;
}
});
this.pngQuantProcess.once('exit', (exitCode) => {
if (this.hasEnded) {
return;
}
if (exitCode > 0 && !this.hasEnded) {
this._error(
new Error(
`The pngquant process exited with a non-zero exit code: ${exitCode}`
)
);
this.hasEnded = true;
}
});
this.pngQuantProcess.stdout
.on('data', (chunk) => {
this.seenDataOnStdout = true;
this.emit('data', chunk);
})
.once('end', () => {
this.pngQuantProcess = null;
if (!this.hasEnded) {
if (this.seenDataOnStdout) {
this.emit('end');
} else {
this._error(
new Error(
'PngQuant: The stdout stream ended without emitting any data'
)
);
}
this.hasEnded = true;
}
});
if (this.pauseStdoutOfPngQuantProcessAfterStartingIt) {
this.pngQuantProcess.stdout.pause();
}
this.bufferedChunks.forEach(function (bufferedChunk) {
// In node.js 12.6.0+ the error event be invoked syncronously, which means
// that we might clean up between the chunks. Guard against dereferencing
// 'stdin' of null in that case:
if (this.pngQuantProcess) {
if (bufferedChunk === null) {
this.pngQuantProcess.stdin.end();
} else {
this.pngQuantProcess.stdin.write(bufferedChunk);
}
}
}, this);
this.bufferedChunks = null;
});
}
if (this.bufferedChunks) {
this.bufferedChunks.push(chunk);
} else {
this.pngQuantProcess.stdin.write(chunk);
}
};
PngQuant.prototype.end = function (chunk) {
if (this.hasEnded) {
return;
}
if (chunk) {
this.write(chunk);
} else if (!this.pngQuantProcess) {
// No chunks have been rewritten. Write an empty one to make sure there's pngquant process.
this.write(Buffer.from([]));
}
if (this.bufferedChunks) {
this.bufferedChunks.push(null);
} else {
this.pngQuantProcess.stdin.end();
}
};
PngQuant.prototype.pause = function () {
if (this.pngQuantProcess) {
this.pngQuantProcess.stdout.pause();
} else {
this.pauseStdoutOfPngQuantProcessAfterStartingIt = true;
}
};
PngQuant.prototype.resume = function () {
if (this.pngQuantProcess) {
this.pngQuantProcess.stdout.resume();
} else {
this.pauseStdoutOfPngQuantProcessAfterStartingIt = false;
}
};
export { PngQuant as default };