CLI code review

This commit is contained in:
Surma
2020-12-08 16:33:19 +00:00
parent 33c3fd3278
commit 5af8810e0b
10 changed files with 114 additions and 101 deletions

BIN
cli/.DS_Store vendored Normal file

Binary file not shown.

View File

@@ -1,12 +1,12 @@
import { promises as fsp } from "fs"; import { promises as fsp } from 'fs';
const prefix = "json:"; const prefix = 'json:';
const reservedKeys = ["public"]; const reservedKeys = ['public'];
export default function jsonPlugin() { export default function jsonPlugin() {
return { return {
name: "json-plugin", name: 'json-plugin',
async resolveId(id, importer) { async resolveId(id, importer) {
if (!id.startsWith(prefix)) return; if (!id.startsWith(prefix)) return;
const realId = id.slice(prefix.length); const realId = id.slice(prefix.length);
@@ -21,18 +21,18 @@ export default function jsonPlugin() {
async load(id) { async load(id) {
if (!id.startsWith(prefix)) return; if (!id.startsWith(prefix)) return;
const realId = id.slice(prefix.length); const realId = id.slice(prefix.length);
const source = await fsp.readFile(realId, "utf8"); const source = await fsp.readFile(realId, 'utf8');
let code = ""; let code = '';
for (const [key, value] of Object.entries(JSON.parse(source))) { for (const [key, value] of Object.entries(JSON.parse(source))) {
if (reservedKeys.includes(key)) { if (reservedKeys.includes(key)) {
continue; continue;
} }
code += ` code += `
export const ${key} = ${JSON.stringify(value)}; export const ${key} = ${JSON.stringify(value)};
`; `;
} }
return code; return code;
} },
}; };
} }

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

BIN
cli/out/830A0062.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 65 KiB

View File

@@ -1,13 +1,16 @@
import { fileURLToPath } from 'url';
export function pathify(path) { export function pathify(path) {
if (path.startsWith("file://")) { if (path.startsWith('file://')) {
path = path.slice("file://".length); path = fileURLToPath(path);
} }
return path; return path;
} }
export function instantiateEmscriptenWasm(factory, path) { export function instantiateEmscriptenWasm(factory, path) {
return factory({ return factory({
locateFile() { locateFile() {
return pathify(path); return pathify(path);
} },
}); });
} }

View File

@@ -1,16 +1,16 @@
import { program } from "commander"; import { program } from 'commander';
import JSON5 from "json5"; import JSON5 from 'json5';
import { isMainThread } from "worker_threads"; import { isMainThread } from 'worker_threads';
import { cpus } from "os"; import { cpus } from 'os';
import { extname, join, basename } from "path"; import { extname, join, basename } from 'path';
import { promises as fsp } from "fs"; import { promises as fsp } from 'fs';
import { version } from "json:../package.json"; import { version } from 'json:../package.json';
import ora from "ora"; import ora from 'ora';
import kleur from "kleur"; 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 WorkerPool from './worker_pool.js';
import { autoOptimize } from "./auto-optimizer.js"; import { autoOptimize } from './auto-optimizer.js';
function clamp(v, min, max) { function clamp(v, min, max) {
if (v < min) return min; if (v < min) return min;
@@ -18,7 +18,7 @@ function clamp(v, min, max) {
return v; return v;
} }
const suffix = ["B", "KB", "MB"]; const suffix = ['B', 'KB', 'MB'];
function prettyPrintSize(size) { function prettyPrintSize(size) {
const base = Math.floor(Math.log2(size) / 10); const base = Math.floor(Math.log2(size) / 10);
const index = clamp(base, 0, 2); const index = clamp(base, 0, 2);
@@ -29,21 +29,21 @@ async function decodeFile(file) {
const buffer = await fsp.readFile(file); const buffer = await fsp.readFile(file);
const firstChunk = buffer.slice(0, 16); const firstChunk = buffer.slice(0, 16);
const firstChunkString = Array.from(firstChunk) const firstChunkString = Array.from(firstChunk)
.map(v => String.fromCodePoint(v)) .map((v) => String.fromCodePoint(v))
.join(""); .join('');
const key = Object.entries(supportedFormats).find(([name, { detectors }]) => const key = Object.entries(supportedFormats).find(([name, { detectors }]) =>
detectors.some(detector => detector.exec(firstChunkString)) detectors.some((detector) => detector.exec(firstChunkString)),
)?.[0]; )?.[0];
if (!key) { if (!key) {
throw Error(`${file} has an unsupported format`); throw Error(`${file} has an unsupported format`);
} }
const rgba = (await supportedFormats[key].dec()).decode( const rgba = (await supportedFormats[key].dec()).decode(
new Uint8Array(buffer) new Uint8Array(buffer),
); );
return { return {
file, file,
bitmap: rgba, bitmap: rgba,
size: buffer.length size: buffer.length,
}; };
} }
@@ -53,7 +53,7 @@ async function preprocessImage({ preprocessorName, options, file }) {
file.bitmap.data, file.bitmap.data,
file.bitmap.width, file.bitmap.width,
file.bitmap.height, file.bitmap.height,
options options,
); );
return file; return file;
} }
@@ -66,11 +66,11 @@ async function encodeFile({
encName, encName,
encConfig, encConfig,
optimizerButteraugliTarget, optimizerButteraugliTarget,
maxOptimizerRounds maxOptimizerRounds,
}) { }) {
let out, infoText; let out, infoText;
const encoder = await supportedFormats[encName].enc(); const encoder = await supportedFormats[encName].enc();
if (encConfig === "auto") { if (encConfig === 'auto') {
const optionToOptimize = supportedFormats[encName].autoOptimize.option; const optionToOptimize = supportedFormats[encName].autoOptimize.option;
const decoder = await supportedFormats[encName].dec(); const decoder = await supportedFormats[encName].dec();
const encode = (bitmapIn, quality) => const encode = (bitmapIn, quality) =>
@@ -79,10 +79,10 @@ async function encodeFile({
bitmapIn.width, bitmapIn.width,
bitmapIn.height, bitmapIn.height,
Object.assign({}, supportedFormats[encName].defaultEncoderOptions, { Object.assign({}, supportedFormats[encName].defaultEncoderOptions, {
[optionToOptimize]: quality [optionToOptimize]: quality,
}) }),
); );
const decode = binary => decoder.decode(binary); const decode = (binary) => decoder.decode(binary);
const { bitmap, binary, quality } = await autoOptimize( const { bitmap, binary, quality } = await autoOptimize(
bitmapIn, bitmapIn,
encode, encode,
@@ -91,13 +91,13 @@ async function encodeFile({
min: supportedFormats[encName].autoOptimize.min, min: supportedFormats[encName].autoOptimize.min,
max: supportedFormats[encName].autoOptimize.max, max: supportedFormats[encName].autoOptimize.max,
butteraugliDistanceGoal: optimizerButteraugliTarget, butteraugliDistanceGoal: optimizerButteraugliTarget,
maxRounds: maxOptimizerRounds maxRounds: maxOptimizerRounds,
} },
); );
out = binary; out = binary;
const opts = { const opts = {
// 5 significant digits is enough // 5 significant digits is enough
[optionToOptimize]: Math.round(quality * 10000) / 10000 [optionToOptimize]: Math.round(quality * 10000) / 10000,
}; };
infoText = ` using --${encName} '${JSON5.stringify(opts)}'`; infoText = ` using --${encName} '${JSON5.stringify(opts)}'`;
} else { } else {
@@ -105,7 +105,7 @@ async function encodeFile({
bitmapIn.data.buffer, bitmapIn.data.buffer,
bitmapIn.width, bitmapIn.width,
bitmapIn.height, bitmapIn.height,
encConfig encConfig,
); );
} }
await fsp.writeFile(outputFile, out); await fsp.writeFile(outputFile, out);
@@ -114,7 +114,7 @@ async function encodeFile({
inputSize: size, inputSize: size,
inputFile: file, inputFile: file,
outputFile, outputFile,
outputSize: out.length outputSize: out.length,
}; };
} }
@@ -122,11 +122,11 @@ async function encodeFile({
function handleJob(params) { function handleJob(params) {
const { operation } = params; const { operation } = params;
switch (operation) { switch (operation) {
case "encode": case 'encode':
return encodeFile(params); return encodeFile(params);
case "decode": case 'decode':
return decodeFile(params.file); return decodeFile(params.file);
case "preprocess": case 'preprocess':
return preprocessImage(params); return preprocessImage(params);
default: default:
throw Error(`Invalid job "${operation}"`); throw Error(`Invalid job "${operation}"`);
@@ -139,44 +139,44 @@ function progressTracker(results) {
tracker.spinner = spinner; tracker.spinner = spinner;
tracker.progressOffset = 0; tracker.progressOffset = 0;
tracker.totalOffset = 0; tracker.totalOffset = 0;
let status = ""; let status = '';
tracker.setStatus = text => { tracker.setStatus = (text) => {
status = text || ""; status = text || '';
update(); update();
}; };
let progress = ""; let progress = '';
tracker.setProgress = (done, total) => { tracker.setProgress = (done, total) => {
spinner.prefixText = kleur.dim(`${done}/${total}`); spinner.prefixText = kleur.dim(`${done}/${total}`);
const completeness = const completeness =
(tracker.progressOffset + done) / (tracker.totalOffset + total); (tracker.progressOffset + done) / (tracker.totalOffset + total);
progress = kleur.cyan( progress = kleur.cyan(
`${"▨".repeat((completeness * 10) | 0).padEnd(10, "╌")}` `${'▨'.repeat((completeness * 10) | 0).padEnd(10, '╌')}`,
); );
update(); update();
}; };
function update() { function update() {
spinner.text = progress + kleur.bold(status) + getResultsText(); spinner.text = progress + kleur.bold(status) + getResultsText();
} }
tracker.finish = text => { tracker.finish = (text) => {
spinner.succeed(kleur.bold(text) + getResultsText()); spinner.succeed(kleur.bold(text) + getResultsText());
}; };
function getResultsText() { function getResultsText() {
let out = ""; let out = '';
for (const [filename, result] of results.entries()) { for (const [filename, result] of results.entries()) {
out += `\n ${kleur.cyan(filename)}: ${prettyPrintSize(result.size)}`; out += `\n ${kleur.cyan(filename)}: ${prettyPrintSize(result.size)}`;
for (const { outputFile, outputSize, infoText } of result.outputs) { for (const { outputFile, outputSize, infoText } of result.outputs) {
const name = (program.suffix + extname(outputFile)).padEnd(5); const name = (program.suffix + extname(outputFile)).padEnd(5);
out += `\n ${kleur.dim("└")} ${kleur.cyan(name)}${prettyPrintSize( out += `\n ${kleur.dim('└')} ${kleur.cyan(name)}${prettyPrintSize(
outputSize outputSize,
)}`; )}`;
const percent = ((outputSize / result.size) * 100).toPrecision(3); const percent = ((outputSize / result.size) * 100).toPrecision(3);
out += ` (${kleur[outputSize > result.size ? "red" : "green"]( out += ` (${kleur[outputSize > result.size ? 'red' : 'green'](
percent + "%" percent + '%',
)})`; )})`;
if (infoText) out += kleur.yellow(infoText); if (infoText) out += kleur.yellow(infoText);
} }
} }
return out || "\n"; return out || '\n';
} }
spinner.start(); spinner.start();
return tracker; return tracker;
@@ -188,7 +188,7 @@ async function processFiles(files) {
const results = new Map(); const results = new Map();
const progress = progressTracker(results); const progress = progressTracker(results);
progress.setStatus("Decoding..."); progress.setStatus('Decoding...');
progress.totalOffset = files.length; progress.totalOffset = files.length;
progress.setProgress(0, files.length); progress.setProgress(0, files.length);
@@ -198,19 +198,19 @@ async function processFiles(files) {
let decoded = 0; let decoded = 0;
let decodedFiles = await Promise.all( let decodedFiles = await Promise.all(
files.map(async file => { files.map(async (file) => {
const result = await workerPool.dispatchJob({ const result = await workerPool.dispatchJob({
operation: "decode", operation: 'decode',
file file,
}); });
results.set(file, { results.set(file, {
file: result.file, file: result.file,
size: result.size, size: result.size,
outputs: [] outputs: [],
}); });
progress.setProgress(++decoded, files.length); progress.setProgress(++decoded, files.length);
return result; return result;
}) }),
); );
for (const [preprocessorName, value] of Object.entries(preprocessors)) { for (const [preprocessorName, value] of Object.entries(preprocessors)) {
@@ -221,26 +221,23 @@ async function processFiles(files) {
const preprocessorOptions = Object.assign( const preprocessorOptions = Object.assign(
{}, {},
value.defaultOptions, value.defaultOptions,
JSON5.parse(preprocessorParam) JSON5.parse(preprocessorParam),
); );
decodedFiles = await Promise.all( decodedFiles = await Promise.all(
decodedFiles.map(async file => { decodedFiles.map(async (file) => {
return workerPool.dispatchJob({ return workerPool.dispatchJob({
file, file,
operation: "preprocess", operation: 'preprocess',
preprocessorName, preprocessorName,
options: preprocessorOptions options: preprocessorOptions,
}); });
}) }),
); );
for (const { file, bitmap, size } of decodedFiles) {
}
} }
progress.progressOffset = decoded; progress.progressOffset = decoded;
progress.setStatus("Encoding " + kleur.dim(`(${parallelism} threads)`)); progress.setStatus('Encoding ' + kleur.dim(`(${parallelism} threads)`));
progress.setProgress(0, files.length); progress.setProgress(0, files.length);
const jobs = []; const jobs = [];
@@ -255,20 +252,20 @@ async function processFiles(files) {
continue; continue;
} }
const encParam = const encParam =
typeof program[encName] === "string" ? program[encName] : "{}"; typeof program[encName] === 'string' ? program[encName] : '{}';
const encConfig = const encConfig =
encParam.toLowerCase() === "auto" encParam.toLowerCase() === 'auto'
? "auto" ? 'auto'
: Object.assign( : Object.assign(
{}, {},
value.defaultEncoderOptions, value.defaultEncoderOptions,
JSON5.parse(encParam) JSON5.parse(encParam),
); );
const outputFile = join(program.outputDir, `${base}.${value.extension}`); const outputFile = join(program.outputDir, `${base}.${value.extension}`);
jobsStarted++; jobsStarted++;
const p = workerPool const p = workerPool
.dispatchJob({ .dispatchJob({
operation: "encode", operation: 'encode',
file, file,
size, size,
bitmap, bitmap,
@@ -276,11 +273,11 @@ async function processFiles(files) {
encName, encName,
encConfig, encConfig,
optimizerButteraugliTarget: Number( optimizerButteraugliTarget: Number(
program.optimizerButteraugliTarget program.optimizerButteraugliTarget,
), ),
maxOptimizerRounds: Number(program.maxOptimizerRounds) maxOptimizerRounds: Number(program.maxOptimizerRounds),
}) })
.then(output => { .then((output) => {
jobsFinished++; jobsFinished++;
results.get(file).outputs.push(output); results.get(file).outputs.push(output);
progress.setProgress(jobsFinished, jobsStarted); progress.setProgress(jobsFinished, jobsStarted);
@@ -294,25 +291,25 @@ async function processFiles(files) {
// Wait for all jobs to finish // Wait for all jobs to finish
await workerPool.join(); await workerPool.join();
await Promise.all(jobs); await Promise.all(jobs);
progress.finish("Squoosh results:"); progress.finish('Squoosh results:');
} }
if (isMainThread) { if (isMainThread) {
program program
.name("squoosh-cli") .name('squoosh-cli')
.version(version) .version(version)
.arguments("<files...>") .arguments('<files...>')
.option("-d, --output-dir <dir>", "Output directory", ".") .option('-d, --output-dir <dir>', 'Output directory', '.')
.option("-s, --suffix <suffix>", "Append suffix to output files", "") .option('-s, --suffix <suffix>', 'Append suffix to output files', '')
.option( .option(
"--max-optimizer-rounds <rounds>", '--max-optimizer-rounds <rounds>',
"Maximum number of compressions to use for auto optimizations", 'Maximum number of compressions to use for auto optimizations',
"6" '6',
) )
.option( .option(
"--optimizer-butteraugli-target <butteraugli distance>", '--optimizer-butteraugli-target <butteraugli distance>',
"Target Butteraugli distance for auto optimizer", 'Target Butteraugli distance for auto optimizer',
"1.4" '1.4',
) )
.action(processFiles); .action(processFiles);
@@ -324,7 +321,7 @@ if (isMainThread) {
for (const [key, value] of Object.entries(supportedFormats)) { for (const [key, value] of Object.entries(supportedFormats)) {
program.option( program.option(
`--${key} [config]`, `--${key} [config]`,
`Use ${value.name} to generate a .${value.extension} file with the given configuration` `Use ${value.name} to generate a .${value.extension} file with the given configuration`,
); );
} }

View File

@@ -1,23 +1,24 @@
import { Worker, parentPort } from "worker_threads"; import { Worker, parentPort } from 'worker_threads';
import { TransformStream } from "web-streams-polyfill"; import { TransformStream } from 'web-streams-polyfill';
function uuid() { function uuid() {
return Array.from({ length: 16 }, () => return Array.from({ length: 16 }, () =>
Math.floor(Math.random() * 256).toString(16) Math.floor(Math.random() * 256).toString(16),
).join(""); ).join('');
} }
function jobPromise(worker, msg) { function jobPromise(worker, msg) {
return new Promise(resolve => { return new Promise((resolve) => {
const id = uuid(); const id = uuid();
worker.postMessage({ msg, id }); worker.postMessage({ msg, id });
worker.on("message", function f({ result, id: rid }) { worker.on('message', function f({ result, id: rid }) {
if (rid !== id) { if (rid !== id) {
return; return;
} }
worker.off("message", f); worker.off('message', f);
resolve(result); resolve(result);
}); });
worker.on('error', (error) => console.error('Worker error: ', error));
}); });
} }
@@ -47,7 +48,7 @@ export default class WorkerPool {
} }
const { msg, resolve } = value; const { msg, resolve } = value;
const worker = await this._nextWorker(); const worker = await this._nextWorker();
jobPromise(worker, msg).then(result => { jobPromise(worker, msg).then((result) => {
resolve(result); resolve(result);
// If we are in the process of closing, `workerQueue` is // If we are in the process of closing, `workerQueue` is
// already closed and we cant requeue the worker. // already closed and we cant requeue the worker.
@@ -86,7 +87,7 @@ export default class WorkerPool {
} }
dispatchJob(msg) { dispatchJob(msg) {
return new Promise(resolve => { return new Promise((resolve) => {
const writer = this.jobQueue.writable.getWriter(); const writer = this.jobQueue.writable.getWriter();
writer.write({ msg, resolve }); writer.write({ msg, resolve });
writer.releaseLock(); writer.releaseLock();
@@ -94,7 +95,7 @@ export default class WorkerPool {
} }
static useThisThreadAsWorker(cb) { static useThisThreadAsWorker(cb) {
parentPort.on("message", async data => { parentPort.on('message', async (data) => {
const { msg, id } = data; const { msg, id } = data;
const result = await cb(msg); const result = await cb(msg);
parentPort.postMessage({ result, id }); parentPort.postMessage({ result, id });

12
cli/tmp.txt Normal file

File diff suppressed because one or more lines are too long