diff --git a/cli/src/codecs.js b/cli/src/codecs.js index 9c93dce8..d38b4f5c 100644 --- a/cli/src/codecs.js +++ b/cli/src/codecs.js @@ -26,14 +26,21 @@ const pngEncDecPromise = pngEncDec.default( fsp.readFile(pathify(pngEncDecWasm)) ); +// OxiPNG import * as oxipng from "../../codecs/oxipng/pkg/squoosh_oxipng.js"; import oxipngWasm from "asset-url:../../codecs/oxipng/pkg/squoosh_oxipng_bg.wasm"; const oxipngPromise = oxipng.default(fsp.readFile(pathify(oxipngWasm))); +// Resize import * as resize from "../../codecs/resize/pkg/squoosh_resize.js"; import resizeWasm from "asset-url:../../codecs/resize/pkg/squoosh_resize_bg.wasm"; const resizePromise = resize.default(fsp.readFile(pathify(resizeWasm))); +// ImageQuant +import imageQuant from "../../codecs/imagequant/imagequant.js"; +import imageQuantWasm from "asset-url:../../codecs/imagequant/imagequant.wasm"; +const imageQuantPromise = instantiateEmscriptenWasm(imageQuant, imageQuantWasm); + // Our decoders currently rely on a `ImageData` global. import ImageData from "./image_data.js"; globalThis.ImageData = ImageData; @@ -114,12 +121,28 @@ export const preprocessors = { }; }, defaultOptions: { - // This will be set to 'vector' if the input is SVG. method: "lanczos3", fitMethod: "stretch", premultiply: true, linearRGB: true } + }, + quant: { + name: "ImageQuant", + description: "Reduce the number of colors used (aka. paletting)", + instantiate: async () => { + const imageQuant = await imageQuantPromise; + return (buffer, width, height, { numColors, dither }) => + new ImageData( + imageQuant.quantize(buffer, width, height, numColors, dither), + width, + height + ); + }, + defaultOptions: { + numColors: 255, + dither: 1.0 + } } }; diff --git a/cli/src/index.js b/cli/src/index.js index 0cb55eed..ce67af0c 100644 --- a/cli/src/index.js +++ b/cli/src/index.js @@ -5,10 +5,10 @@ import { cpus } from "os"; import { extname, join, basename } from "path"; import { promises as fsp } from "fs"; import { version } from "json:../package.json"; -import ora from 'ora'; -import kleur from 'kleur'; +import ora from "ora"; +import kleur from "kleur"; -import {codecs as supportedFormats, preprocessors} from "./codecs.js"; +import { codecs as supportedFormats, preprocessors } from "./codecs.js"; import WorkerPool from "./worker_pool.js"; import { autoOptimize } from "./auto-optimizer.js"; @@ -47,14 +47,15 @@ async function decodeFile(file) { }; } -async function preprocessImage({ - preprocessorName, - options, - file -}) { - const preprocessor = await preprocessors[preprocessorName].instantiate(); - file.bitmap= await preprocessor(file.bitmap.data, file.bitmap.width, file.bitmap.height, options); - return file; +async function preprocessImage({ preprocessorName, options, file }) { + const preprocessor = await preprocessors[preprocessorName].instantiate(); + file.bitmap = await preprocessor( + file.bitmap.data, + file.bitmap.width, + file.bitmap.height, + options + ); + return file; } async function encodeFile({ @@ -120,16 +121,16 @@ async function encodeFile({ // both decoding and encoding go through the worker pool function handleJob(params) { const { operation } = params; - switch(operation) { - case "encode": - return encodeFile(params); - case "decode": - return decodeFile(params.file); - case "preprocess": - return preprocessImage(params); - default: - throw Error(`Invalid job "${operation}"`); - } + switch (operation) { + case "encode": + return encodeFile(params); + case "decode": + return decodeFile(params.file); + case "preprocess": + return preprocessImage(params); + default: + throw Error(`Invalid job "${operation}"`); + } } function progressTracker(results) { @@ -138,37 +139,44 @@ function progressTracker(results) { tracker.spinner = spinner; tracker.progressOffset = 0; tracker.totalOffset = 0; - let status = ''; - tracker.setStatus = (text) => { - status = text || ''; + let status = ""; + tracker.setStatus = text => { + status = text || ""; update(); }; - let progress = ''; + let progress = ""; tracker.setProgress = (done, total) => { spinner.prefixText = kleur.dim(`${done}/${total}`); - const completeness = (tracker.progressOffset + done) / (tracker.totalOffset + total); - progress = kleur.cyan(`▐${'▨'.repeat(completeness*10|0).padEnd(10, '╌')}▌ `); + const completeness = + (tracker.progressOffset + done) / (tracker.totalOffset + total); + progress = kleur.cyan( + `▐${"▨".repeat((completeness * 10) | 0).padEnd(10, "╌")}▌ ` + ); update(); }; function update() { spinner.text = progress + kleur.bold(status) + getResultsText(); } - tracker.finish = (text) => { + tracker.finish = text => { spinner.succeed(kleur.bold(text) + getResultsText()); - } + }; function getResultsText() { - let out = ''; + let out = ""; for (const [filename, result] of results.entries()) { out += `\n ${kleur.cyan(filename)}: ${prettyPrintSize(result.size)}`; for (const { outputFile, outputSize, infoText } of result.outputs) { const name = (program.suffix + extname(outputFile)).padEnd(5); - out += `\n ${kleur.dim('└')} ${kleur.cyan(name)} → ${prettyPrintSize(outputSize)}`; + out += `\n ${kleur.dim("└")} ${kleur.cyan(name)} → ${prettyPrintSize( + outputSize + )}`; const percent = ((outputSize / result.size) * 100).toPrecision(3); - out += ` (${kleur[outputSize>result.size?'red':'green'](percent+'%')})`; + out += ` (${kleur[outputSize > result.size ? "red" : "green"]( + percent + "%" + )})`; if (infoText) out += kleur.yellow(infoText); } } - return out || '\n'; + return out || "\n"; } spinner.start(); return tracker; @@ -180,7 +188,7 @@ async function processFiles(files) { const results = new Map(); const progress = progressTracker(results); - progress.setStatus('Decoding...'); + progress.setStatus("Decoding..."); progress.totalOffset = files.length; progress.setProgress(0, files.length); @@ -189,44 +197,50 @@ async function processFiles(files) { await fsp.mkdir(program.outputDir, { recursive: true }); let decoded = 0; - let decodedFiles = await Promise.all(files.map(async file => { - const result = await workerPool.dispatchJob({ operation: 'decode', file }); - results.set(file, { - file: result.file, - size: result.size, - outputs: [] - }); - progress.setProgress(++decoded, files.length); - return result; - })); + let decodedFiles = await Promise.all( + files.map(async file => { + const result = await workerPool.dispatchJob({ + operation: "decode", + file + }); + results.set(file, { + file: result.file, + size: result.size, + outputs: [] + }); + progress.setProgress(++decoded, files.length); + return result; + }) + ); - for (const [preprocessorName, value] of Object.entries(preprocessors)) { - if(!program[preprocessorName]) { - continue; - } - const preprocessorParam = program[preprocessorName]; - const preprocessorOptions = Object.assign( - {}, - value.defaultOptions, - JSON5.parse(preprocessorParam) - ); + for (const [preprocessorName, value] of Object.entries(preprocessors)) { + if (!program[preprocessorName]) { + continue; + } + const preprocessorParam = program[preprocessorName]; + const preprocessorOptions = Object.assign( + {}, + value.defaultOptions, + JSON5.parse(preprocessorParam) + ); - decodedFiles = await Promise.all(decodedFiles.map(async file => { - return workerPool.dispatchJob({ - file, - operation: "preprocess", - preprocessorName, - options: preprocessorOptions - }); - })); - - for (const { file, bitmap, size } of decodedFiles) { - - } - } + decodedFiles = await Promise.all( + decodedFiles.map(async file => { + return workerPool.dispatchJob({ + file, + operation: "preprocess", + preprocessorName, + options: preprocessorOptions + }); + }) + ); + + for (const { file, bitmap, size } of decodedFiles) { + } + } progress.progressOffset = decoded; - progress.setStatus('Encoding ' + kleur.dim(`(${parallelism} threads)`)); + progress.setStatus("Encoding " + kleur.dim(`(${parallelism} threads)`)); progress.setProgress(0, files.length); const jobs = []; @@ -254,17 +268,19 @@ async function processFiles(files) { jobsStarted++; const p = workerPool .dispatchJob({ - operation: 'encode', + operation: "encode", file, size, bitmap, outputFile, encName, encConfig, - optimizerButteraugliTarget: Number(program.optimizerButteraugliTarget), + optimizerButteraugliTarget: Number( + program.optimizerButteraugliTarget + ), maxOptimizerRounds: Number(program.maxOptimizerRounds) }) - .then((output) => { + .then(output => { jobsFinished++; results.get(file).outputs.push(output); progress.setProgress(jobsFinished, jobsStarted); @@ -278,7 +294,7 @@ async function processFiles(files) { // Wait for all jobs to finish await workerPool.join(); await Promise.all(jobs); - progress.finish('Squoosh results:'); + progress.finish("Squoosh results:"); } if (isMainThread) { @@ -291,21 +307,18 @@ if (isMainThread) { .option( "--max-optimizer-rounds ", "Maximum number of compressions to use for auto optimizations", - '6' + "6" ) .option( "--optimizer-butteraugli-target ", "Target Butteraugli distance for auto optimizer", - '1.4' + "1.4" ) .action(processFiles); // Create a CLI option for each supported preprocessor for (const [key, value] of Object.entries(preprocessors)) { - program.option( - `--${key} [config]`, - value.description - ); + program.option(`--${key} [config]`, value.description); } // Create a CLI option for each supported encoder for (const [key, value] of Object.entries(supportedFormats)) {