siuuu 0.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,38 @@
1
+ # Siuuu
2
+
3
+ Compress images (PNG / JPEG / WebP) with WebAssembly codecs (imagequant + oxipng, mozjpeg, libwebp).
4
+
5
+ ## Usage
6
+
7
+ ```bash
8
+ npx siuuu <files or directories...> [options]
9
+ ```
10
+
11
+ Options:
12
+
13
+ - `-o, --out <filename>` — output filename for the input right before it (repeatable)
14
+ - `-d, --out-dir <dir>` — output directory (default: `siuuu-output/` in the current directory)
15
+ - `-f, --format <format>` — output format: `png` / `jpeg` / `webp` (default: same as input)
16
+ - `-h, --help` — show help
17
+ - `-v, --version` — show version
18
+
19
+ Inputs without an explicit `-o` are written to the output directory under their original name; existing files are never overwritten (falls back to `name (n)`). With `-f`, every output is converted to that format; `-o` only sets the filename, not the format.
20
+
21
+ ## Examples
22
+
23
+ ```bash
24
+ # Compress one or more files (default: ./siuuu-output/, original names)
25
+ npx siuuu a.png b.jpg c.webp
26
+
27
+ # Recursively compress all images in a directory
28
+ npx siuuu photos/
29
+
30
+ # Convert everything to WebP
31
+ npx siuuu photos/ -f webp
32
+
33
+ # Per-file output names (-o binds to the input before it)
34
+ npx siuuu a.png -o x.png b.png -o y.png
35
+
36
+ # Custom output directory
37
+ npx siuuu imgs/ -d dist/
38
+ ```
package/dist/cli.d.mts ADDED
@@ -0,0 +1 @@
1
+ export { };
package/dist/cli.mjs ADDED
@@ -0,0 +1,299 @@
1
+ #!/usr/bin/env node
2
+ import { t as compress } from "./src-CqREwc6F.mjs";
3
+ import { createRequire } from "node:module";
4
+ import { mkdir, readFile, readdir, stat, writeFile } from "node:fs/promises";
5
+ import { basename, dirname, extname, isAbsolute, join, relative, resolve } from "node:path";
6
+ import { parseArgs } from "node:util";
7
+ import * as os from "node:os";
8
+ import { Worker } from "node:worker_threads";
9
+ //#region src/batch.ts
10
+ const IMAGE_EXTENSIONS = new Set([
11
+ ".png",
12
+ ".jpg",
13
+ ".jpeg",
14
+ ".webp"
15
+ ]);
16
+ const OUTPUT_EXTENSION = {
17
+ png: "png",
18
+ jpeg: "jpg",
19
+ webp: "webp"
20
+ };
21
+ function formatSize(bytes) {
22
+ if (bytes < 1024) return `${bytes} B`;
23
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
24
+ return `${(bytes / 1024 / 1024).toFixed(2)} MB`;
25
+ }
26
+ function isImage(path) {
27
+ return IMAGE_EXTENSIONS.has(extname(path).toLowerCase());
28
+ }
29
+ async function isDirectory(path) {
30
+ try {
31
+ return (await stat(path)).isDirectory();
32
+ } catch {
33
+ return false;
34
+ }
35
+ }
36
+ async function exists(path) {
37
+ try {
38
+ await stat(path);
39
+ return true;
40
+ } catch {
41
+ return false;
42
+ }
43
+ }
44
+ function isInside(file, dir) {
45
+ const rel = relative(resolve(dir), resolve(file));
46
+ return rel === "" || !rel.startsWith("..") && !isAbsolute(rel);
47
+ }
48
+ async function listImages(dir) {
49
+ return (await readdir(dir, { recursive: true })).filter((name) => isImage(name)).map((name) => join(dir, name)).sort();
50
+ }
51
+ const workerUrl = new URL(import.meta.url.endsWith(".ts") ? "./worker.ts" : "./worker.mjs", import.meta.url);
52
+ function compressOnWorker(worker, task) {
53
+ return new Promise((resolve, reject) => {
54
+ const cleanup = () => {
55
+ worker.off("message", onMessage);
56
+ worker.off("error", onError);
57
+ worker.off("exit", onExit);
58
+ };
59
+ const onMessage = (msg) => {
60
+ cleanup();
61
+ resolve(msg);
62
+ };
63
+ const onError = (error) => {
64
+ cleanup();
65
+ reject(error);
66
+ };
67
+ const onExit = (code) => {
68
+ cleanup();
69
+ reject(/* @__PURE__ */ new Error(`worker 异常退出(code ${code})`));
70
+ };
71
+ worker.on("message", onMessage);
72
+ worker.on("error", onError);
73
+ worker.on("exit", onExit);
74
+ worker.postMessage(task);
75
+ });
76
+ }
77
+ async function runBatch(jobs, handlers) {
78
+ const done = /* @__PURE__ */ new Set();
79
+ const persist = async (id, format, data, before) => {
80
+ const outPath = await handlers.resolveOutPath(jobs[id], format);
81
+ await mkdir(dirname(outPath), { recursive: true });
82
+ await writeFile(outPath, data);
83
+ handlers.onWritten?.(jobs[id], {
84
+ before,
85
+ after: data.length,
86
+ outPath
87
+ });
88
+ };
89
+ let writes = Promise.resolve();
90
+ const write = (id, run) => {
91
+ writes = writes.then(async () => {
92
+ try {
93
+ await run();
94
+ } catch (error) {
95
+ handlers.onFailed?.(jobs[id], error.message);
96
+ }
97
+ done.add(id);
98
+ });
99
+ return writes;
100
+ };
101
+ const spawn = () => new Worker(workerUrl, { execArgv: process.execArgv });
102
+ let next = 0;
103
+ let proven = false;
104
+ const consume = async () => {
105
+ let worker = null;
106
+ for (let id = next++; id < jobs.length; id = next++) {
107
+ if (!worker) try {
108
+ worker = spawn();
109
+ } catch {
110
+ return;
111
+ }
112
+ try {
113
+ const result = await compressOnWorker(worker, {
114
+ id,
115
+ source: jobs[id].source,
116
+ format: jobs[id].format
117
+ });
118
+ proven = true;
119
+ await write(id, () => result.ok ? persist(id, result.format, new Uint8Array(result.data), result.before) : Promise.reject(new Error(result.error)));
120
+ } catch (crash) {
121
+ await worker.terminate().catch(() => {});
122
+ worker = null;
123
+ if (!proven) return;
124
+ await write(id, () => Promise.reject(crash));
125
+ }
126
+ }
127
+ await worker?.terminate().catch(() => {});
128
+ };
129
+ const runOnMain = async () => {
130
+ for (let id = 0; id < jobs.length; id++) {
131
+ if (done.has(id)) continue;
132
+ await write(id, async () => {
133
+ const data = await readFile(jobs[id].source);
134
+ const result = await compress(new Uint8Array(data), { format: jobs[id].format });
135
+ await persist(id, result.format, result.data, data.length);
136
+ });
137
+ }
138
+ };
139
+ const parallelism = os.availableParallelism?.() ?? os.cpus().length;
140
+ const concurrency = handlers.concurrency ?? Math.min(jobs.length, Math.max(1, parallelism));
141
+ if (concurrency > 1) {
142
+ await Promise.all(Array.from({ length: concurrency }, consume));
143
+ await writes.catch(() => {});
144
+ const leftover = jobs.length - done.size;
145
+ if (leftover > 0) console.warn(`并行压缩不可用,已回退主线程串行处理 ${leftover} 张`);
146
+ }
147
+ await runOnMain();
148
+ }
149
+ //#endregion
150
+ //#region src/cli.ts
151
+ const pkg = createRequire(import.meta.url)("../package.json");
152
+ const DEFAULT_OUT_DIR = "siuuu-output";
153
+ const HELP = `${pkg.name} v${pkg.version} — 压缩 PNG / JPEG / WebP 图片
154
+
155
+ 用法:
156
+ npx ${pkg.name} <文件或目录...> [选项]
157
+
158
+ 选项:
159
+ -o, --out <文件名> 为紧邻的前一个输入单独指定输出文件名(可多次)
160
+ -d, --out-dir <目录> 全局输出目录(默认:当前目录下的 ${DEFAULT_OUT_DIR}/)
161
+ -f, --format <格式> 输出格式 png | jpeg | webp(默认与输入一致,不做转换)
162
+ -h, --help 显示帮助
163
+ -v, --version 显示版本
164
+
165
+ 说明:
166
+ 传入目录时会递归压缩其中所有 PNG / JPEG / WebP,多文件时自动用多线程并行。
167
+ 未用 -o 单独命名的,按原文件名输出到输出目录;同名不覆盖,自动改用「名称 (n)」。
168
+ 指定 -f 后所有输出都转为该格式;-o 只决定文件名,不影响格式。
169
+ 压缩参数:PNG 高画质(量化 ≤200 色 + oxipng)、JPEG 中画质(mozjpeg 75)、WebP 高画质(libwebp 90)。
170
+
171
+ 示例:
172
+ npx ${pkg.name} a.png b.jpg # 压缩到 ${DEFAULT_OUT_DIR}/a.png、${DEFAULT_OUT_DIR}/b.jpg
173
+ npx ${pkg.name} photos/ -f webp # 递归压缩 photos/ 并全部转 webp
174
+ npx ${pkg.name} a.png -o x.png b.png -o y.png # 分别指定输出文件名
175
+ npx ${pkg.name} imgs/ -d dist/ # 输出到 dist/
176
+ `;
177
+ function parseFormat(value) {
178
+ const v = value.toLowerCase();
179
+ if (v === "png" || v === "jpeg" || v === "webp") return v;
180
+ if (v === "jpg") return "jpeg";
181
+ }
182
+ async function uniqueOutPath(dir, filename, used) {
183
+ const ext = extname(filename);
184
+ const base = basename(filename, ext);
185
+ let candidate = join(dir, filename);
186
+ let n = 1;
187
+ while (used.has(candidate) || await exists(candidate)) {
188
+ candidate = join(dir, `${base} (${n})${ext}`);
189
+ n++;
190
+ }
191
+ used.add(candidate);
192
+ return candidate;
193
+ }
194
+ async function main() {
195
+ let parsed;
196
+ try {
197
+ parsed = parseArgs({
198
+ allowPositionals: true,
199
+ tokens: true,
200
+ options: {
201
+ "out": {
202
+ type: "string",
203
+ short: "o",
204
+ multiple: true
205
+ },
206
+ "out-dir": {
207
+ type: "string",
208
+ short: "d"
209
+ },
210
+ "format": {
211
+ type: "string",
212
+ short: "f"
213
+ },
214
+ "help": {
215
+ type: "boolean",
216
+ short: "h"
217
+ },
218
+ "version": {
219
+ type: "boolean",
220
+ short: "v"
221
+ }
222
+ }
223
+ });
224
+ } catch (error) {
225
+ console.error(`参数错误:${error.message}\n`);
226
+ process.stdout.write(HELP);
227
+ process.exitCode = 1;
228
+ return;
229
+ }
230
+ const { values, positionals, tokens } = parsed;
231
+ if (values.version) {
232
+ console.log(pkg.version);
233
+ return;
234
+ }
235
+ if (values.help || positionals.length === 0) {
236
+ process.stdout.write(HELP);
237
+ if (positionals.length === 0 && !values.help) process.exitCode = 1;
238
+ return;
239
+ }
240
+ let format;
241
+ if (values.format !== void 0) {
242
+ format = parseFormat(values.format);
243
+ if (!format) {
244
+ console.error(`错误:不支持的格式「${values.format}」,可选 png / jpeg / webp`);
245
+ process.exitCode = 1;
246
+ return;
247
+ }
248
+ }
249
+ const outDir = values["out-dir"] ?? DEFAULT_OUT_DIR;
250
+ const specs = [];
251
+ for (const token of tokens) if (token.kind === "positional") specs.push({ input: token.value });
252
+ else if (token.kind === "option" && token.name === "out") {
253
+ const last = specs[specs.length - 1];
254
+ if (!last) {
255
+ console.error("错误:-o 必须跟在某个输入文件之后");
256
+ process.exitCode = 1;
257
+ return;
258
+ }
259
+ last.output = token.value;
260
+ }
261
+ const jobs = [];
262
+ for (const spec of specs) if (await isDirectory(spec.input)) {
263
+ if (spec.output !== void 0) {
264
+ console.error(`错误:目录输入 ${spec.input} 不能用 -o 指定单个输出文件名`);
265
+ process.exitCode = 1;
266
+ return;
267
+ }
268
+ for (const file of await listImages(spec.input)) if (!isInside(file, resolve(outDir))) jobs.push({
269
+ source: file,
270
+ format
271
+ });
272
+ } else jobs.push({
273
+ source: spec.input,
274
+ format,
275
+ output: spec.output
276
+ });
277
+ if (jobs.length === 0) {
278
+ console.error("没有找到可压缩的图片");
279
+ process.exitCode = 1;
280
+ return;
281
+ }
282
+ const used = /* @__PURE__ */ new Set();
283
+ let failed = 0;
284
+ await runBatch(jobs, {
285
+ resolveOutPath: (job, fmt) => job.output ?? uniqueOutPath(outDir, `${basename(job.source, extname(job.source))}.${OUTPUT_EXTENSION[fmt]}`, used),
286
+ onWritten: (job, { before, after, outPath }) => {
287
+ const ratio = before > 0 ? Math.round((1 - after / before) * 100) : 0;
288
+ console.log(`${job.source} ${formatSize(before)} → ${formatSize(after)} (-${ratio}%) → ${outPath}`);
289
+ },
290
+ onFailed: (job, message) => {
291
+ failed++;
292
+ console.error(`${job.source} 失败:${message}`);
293
+ }
294
+ });
295
+ if (failed > 0) process.exitCode = 1;
296
+ }
297
+ main();
298
+ //#endregion
299
+ export {};
@@ -0,0 +1,40 @@
1
+ //#region src/types.d.ts
2
+ type ImageFormat = 'png' | 'jpeg' | 'webp';
3
+ /** 解码后的原始像素,与 jSquash 的 ImageData 结构一致。 */
4
+ interface RawImage {
5
+ data: Uint8Array | Uint8ClampedArray;
6
+ width: number;
7
+ height: number;
8
+ }
9
+ //#endregion
10
+ //#region src/codecs.d.ts
11
+ declare const PRESETS: {
12
+ readonly png: {
13
+ readonly maxColors: 200;
14
+ readonly oxipngLevel: 2;
15
+ };
16
+ readonly jpeg: {
17
+ readonly quality: 75;
18
+ };
19
+ readonly webp: {
20
+ readonly quality: 90;
21
+ readonly method: 6;
22
+ readonly lossless: 0;
23
+ };
24
+ };
25
+ //#endregion
26
+ //#region src/index.d.ts
27
+ interface CompressOptions {
28
+ /** 输出格式,默认与输入一致(第一版不做格式转换)。 */
29
+ format?: ImageFormat;
30
+ }
31
+ interface CompressResult {
32
+ data: Uint8Array;
33
+ format: ImageFormat;
34
+ }
35
+ /** 通过魔数识别图片格式,无法识别时返回 undefined。 */
36
+ declare function detectFormat(data: Uint8Array): ImageFormat | undefined;
37
+ /** 压缩单张图片。 */
38
+ declare function compress(input: Uint8Array, options?: CompressOptions): Promise<CompressResult>;
39
+ //#endregion
40
+ export { CompressOptions, CompressResult, type ImageFormat, PRESETS, type RawImage, compress, detectFormat };
package/dist/index.mjs ADDED
@@ -0,0 +1,2 @@
1
+ import { n as detectFormat, r as PRESETS, t as compress } from "./src-CqREwc6F.mjs";
2
+ export { PRESETS, compress, detectFormat };
@@ -0,0 +1,119 @@
1
+ import { createRequire } from "node:module";
2
+ import { readFile } from "node:fs/promises";
3
+ import jpegDecode, { init } from "@jsquash/jpeg/decode.js";
4
+ import jpegEncode, { init as init$1 } from "@jsquash/jpeg/encode.js";
5
+ import pngDecode, { init as init$2 } from "@jsquash/png/decode.js";
6
+ import webpDecode, { init as init$3 } from "@jsquash/webp/decode.js";
7
+ import webpEncode, { init as init$4 } from "@jsquash/webp/encode.js";
8
+ import oxipngOptimise, { init as init$5 } from "@jsquash/oxipng/optimise.js";
9
+ import * as imagequant from "imagequant/imagequant_bg.js";
10
+ //#region src/codecs.ts
11
+ const PRESETS = {
12
+ png: {
13
+ maxColors: 200,
14
+ oxipngLevel: 2
15
+ },
16
+ jpeg: { quality: 75 },
17
+ webp: {
18
+ quality: 90,
19
+ method: 6,
20
+ lossless: 0
21
+ }
22
+ };
23
+ const require = createRequire(import.meta.url);
24
+ async function wasmBytes(spec) {
25
+ return new Uint8Array(await readFile(require.resolve(spec)));
26
+ }
27
+ async function wasmModule(spec) {
28
+ return WebAssembly.compile(await wasmBytes(spec));
29
+ }
30
+ let jpegDecodeReady;
31
+ let jpegEncodeReady;
32
+ let pngDecodeReady;
33
+ let webpDecodeReady;
34
+ let webpEncodeReady;
35
+ let oxipngReady;
36
+ let imagequantReady;
37
+ async function ensureImagequant() {
38
+ imagequantReady ??= (async () => {
39
+ const { instance } = await WebAssembly.instantiate(await wasmBytes("imagequant/imagequant_bg.wasm"), { "./imagequant_bg.js": imagequant });
40
+ imagequant.__wbg_set_wasm(instance.exports);
41
+ })();
42
+ return imagequantReady;
43
+ }
44
+ function asBuffer(data) {
45
+ return data;
46
+ }
47
+ function asImageData(image) {
48
+ return image;
49
+ }
50
+ function toUint8(data) {
51
+ return new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
52
+ }
53
+ async function decode(data, format) {
54
+ switch (format) {
55
+ case "png":
56
+ pngDecodeReady ??= init$2(await wasmBytes("@jsquash/png/codec/pkg/squoosh_png_bg.wasm"));
57
+ await pngDecodeReady;
58
+ return pngDecode(asBuffer(data));
59
+ case "jpeg":
60
+ jpegDecodeReady ??= init(await wasmModule("@jsquash/jpeg/codec/dec/mozjpeg_dec.wasm"));
61
+ await jpegDecodeReady;
62
+ return jpegDecode(asBuffer(data));
63
+ case "webp":
64
+ webpDecodeReady ??= init$3(await wasmModule("@jsquash/webp/codec/dec/webp_dec.wasm"));
65
+ await webpDecodeReady;
66
+ return webpDecode(asBuffer(data));
67
+ }
68
+ }
69
+ async function encodePng(image) {
70
+ await ensureImagequant();
71
+ const source = imagequant.Imagequant.new_image(toUint8(image.data), image.width, image.height, 0);
72
+ const quantizer = new imagequant.Imagequant();
73
+ quantizer.set_max_colors(PRESETS.png.maxColors);
74
+ const quantized = quantizer.process(source);
75
+ oxipngReady ??= init$5(await wasmBytes("@jsquash/oxipng/codec/pkg/squoosh_oxipng_bg.wasm"));
76
+ await oxipngReady;
77
+ const optimised = await oxipngOptimise(asBuffer(quantized), {
78
+ level: PRESETS.png.oxipngLevel,
79
+ interlace: false
80
+ });
81
+ return new Uint8Array(optimised);
82
+ }
83
+ async function encodeJpeg(image) {
84
+ jpegEncodeReady ??= init$1(await wasmModule("@jsquash/jpeg/codec/enc/mozjpeg_enc.wasm"));
85
+ await jpegEncodeReady;
86
+ return new Uint8Array(await jpegEncode(asImageData(image), { quality: PRESETS.jpeg.quality }));
87
+ }
88
+ async function encodeWebp(image) {
89
+ webpEncodeReady ??= init$4(await wasmModule("@jsquash/webp/codec/enc/webp_enc_simd.wasm"));
90
+ await webpEncodeReady;
91
+ return new Uint8Array(await webpEncode(asImageData(image), { ...PRESETS.webp }));
92
+ }
93
+ async function encode(image, format) {
94
+ switch (format) {
95
+ case "png": return encodePng(image);
96
+ case "jpeg": return encodeJpeg(image);
97
+ case "webp": return encodeWebp(image);
98
+ }
99
+ }
100
+ //#endregion
101
+ //#region src/index.ts
102
+ /** 通过魔数识别图片格式,无法识别时返回 undefined。 */
103
+ function detectFormat(data) {
104
+ if (data.length >= 8 && data[0] === 137 && data[1] === 80 && data[2] === 78 && data[3] === 71) return "png";
105
+ if (data.length >= 3 && data[0] === 255 && data[1] === 216 && data[2] === 255) return "jpeg";
106
+ if (data.length >= 12 && data[0] === 82 && data[1] === 73 && data[2] === 70 && data[3] === 70 && data[8] === 87 && data[9] === 69 && data[10] === 66 && data[11] === 80) return "webp";
107
+ }
108
+ /** 压缩单张图片。 */
109
+ async function compress(input, options = {}) {
110
+ const inputFormat = detectFormat(input);
111
+ if (!inputFormat) throw new Error("无法识别的图片格式,第一版仅支持 PNG / JPEG / WebP");
112
+ const format = options.format ?? inputFormat;
113
+ return {
114
+ data: await encode(await decode(input, inputFormat), format),
115
+ format
116
+ };
117
+ }
118
+ //#endregion
119
+ export { detectFormat as n, PRESETS as r, compress as t };
@@ -0,0 +1 @@
1
+ export { };
@@ -0,0 +1,29 @@
1
+ import { t as compress } from "./src-CqREwc6F.mjs";
2
+ import { readFile } from "node:fs/promises";
3
+ import { parentPort } from "node:worker_threads";
4
+ //#region src/worker.ts
5
+ const port = parentPort;
6
+ if (!port) throw new Error("worker.ts 必须在 worker 线程中运行");
7
+ port.on("message", async (task) => {
8
+ try {
9
+ const data = await readFile(task.source);
10
+ const result = await compress(new Uint8Array(data), { format: task.format });
11
+ const view = result.data;
12
+ const buffer = view.buffer.slice(view.byteOffset, view.byteOffset + view.byteLength);
13
+ port.postMessage({
14
+ id: task.id,
15
+ ok: true,
16
+ format: result.format,
17
+ before: data.length,
18
+ data: buffer
19
+ }, [buffer]);
20
+ } catch (error) {
21
+ port.postMessage({
22
+ id: task.id,
23
+ ok: false,
24
+ error: error.message
25
+ });
26
+ }
27
+ });
28
+ //#endregion
29
+ export {};
package/package.json ADDED
@@ -0,0 +1,53 @@
1
+ {
2
+ "name": "siuuu",
3
+ "type": "module",
4
+ "version": "0.0.0",
5
+ "description": "A CLI that compresses images (PNG / JPEG / WebP) with WebAssembly codecs.",
6
+ "author": "Author Name <author.name@mail.com>",
7
+ "license": "MIT",
8
+ "engines": {
9
+ "node": ">=20"
10
+ },
11
+ "homepage": "https://github.com/WBBB0730/siuu#readme",
12
+ "repository": {
13
+ "type": "git",
14
+ "url": "https://github.com/WBBB0730/siuu.git"
15
+ },
16
+ "exports": {
17
+ ".": "./dist/index.mjs",
18
+ "./cli": "./dist/cli.mjs",
19
+ "./worker": "./dist/worker.mjs",
20
+ "./package.json": "./package.json"
21
+ },
22
+ "bin": {
23
+ "siuuu": "./dist/cli.mjs"
24
+ },
25
+ "files": [
26
+ "dist"
27
+ ],
28
+ "scripts": {
29
+ "build": "tsdown",
30
+ "dev": "tsdown --watch",
31
+ "test": "vitest",
32
+ "play": "tsx scripts/play.ts",
33
+ "typecheck": "tsc --noEmit",
34
+ "release": "bumpp",
35
+ "prepublishOnly": "pnpm run build"
36
+ },
37
+ "devDependencies": {
38
+ "@types/node": "^25.6.2",
39
+ "@typescript/native-preview": "7.0.0-dev.20260509.2",
40
+ "bumpp": "^11.1.0",
41
+ "tsdown": "^0.22.0",
42
+ "tsx": "^4.22.3",
43
+ "typescript": "^6.0.3",
44
+ "vitest": "^4.1.5"
45
+ },
46
+ "dependencies": {
47
+ "@jsquash/jpeg": "^1.6.0",
48
+ "@jsquash/oxipng": "^2.3.0",
49
+ "@jsquash/png": "^3.1.1",
50
+ "@jsquash/webp": "^1.5.0",
51
+ "imagequant": "^0.1.2"
52
+ }
53
+ }