qasai 0.0.1

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/dist/index.js ADDED
@@ -0,0 +1,879 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/index.ts
4
+ import { Command, Option } from "commander";
5
+ import { createRequire } from "module";
6
+
7
+ // src/commands/compress.ts
8
+ import { glob } from "glob";
9
+ import { stat as stat3, mkdir as mkdir3 } from "fs/promises";
10
+ import { join as join3, dirname as dirname3, basename as basename2, relative, resolve } from "path";
11
+ import ora from "ora";
12
+ import pc from "picocolors";
13
+
14
+ // src/utils/compressor.ts
15
+ import sharp from "sharp";
16
+ import { optimize } from "svgo";
17
+ import { readFile as readFile2, writeFile as writeFile2, stat as stat2, mkdir as mkdir2, copyFile as copyFile2 } from "fs/promises";
18
+ import { dirname as dirname2, extname, basename, join as join2 } from "path";
19
+
20
+ // src/utils/engines.ts
21
+ import { execa } from "execa";
22
+ import { stat, copyFile, mkdir } from "fs/promises";
23
+ import { dirname, join } from "path";
24
+ async function compressWithMozjpeg(inputPath, outputPath, options) {
25
+ const originalStats = await stat(inputPath);
26
+ const originalSize = originalStats.size;
27
+ const mozjpeg = await import("mozjpeg");
28
+ const quality = parseInt(options.quality || "80");
29
+ await mkdir(dirname(outputPath), { recursive: true });
30
+ const args = [
31
+ "-quality",
32
+ options.lossless ? "100" : String(quality),
33
+ "-outfile",
34
+ outputPath,
35
+ inputPath
36
+ ];
37
+ if (options.progressive !== false) {
38
+ args.unshift("-progressive");
39
+ }
40
+ await execa(mozjpeg.default, args);
41
+ const compressedStats = await stat(outputPath);
42
+ const compressedSize = compressedStats.size;
43
+ const saved = originalSize - compressedSize;
44
+ return {
45
+ file: inputPath,
46
+ originalSize,
47
+ compressedSize,
48
+ saved,
49
+ savedPercent: saved / originalSize * 100
50
+ };
51
+ }
52
+ async function compressWithJpegtran(inputPath, outputPath, options) {
53
+ const originalStats = await stat(inputPath);
54
+ const originalSize = originalStats.size;
55
+ const jpegtran = await import("jpegtran-bin");
56
+ await mkdir(dirname(outputPath), { recursive: true });
57
+ const args = [
58
+ "-optimize",
59
+ "-outfile",
60
+ outputPath
61
+ ];
62
+ if (options.progressive !== false) {
63
+ args.push("-progressive");
64
+ }
65
+ if (!options.keepMetadata) {
66
+ args.push("-copy", "none");
67
+ } else {
68
+ args.push("-copy", "all");
69
+ }
70
+ args.push(inputPath);
71
+ await execa(jpegtran.default, args);
72
+ const compressedStats = await stat(outputPath);
73
+ const compressedSize = compressedStats.size;
74
+ const saved = originalSize - compressedSize;
75
+ return {
76
+ file: inputPath,
77
+ originalSize,
78
+ compressedSize,
79
+ saved,
80
+ savedPercent: saved / originalSize * 100
81
+ };
82
+ }
83
+ async function compressWithPngquant(inputPath, outputPath, options) {
84
+ const originalStats = await stat(inputPath);
85
+ const originalSize = originalStats.size;
86
+ const pngquant = await import("pngquant-bin");
87
+ const quality = options.pngQuality || options.quality || "65-80";
88
+ const colors = parseInt(options.colors || "256");
89
+ await mkdir(dirname(outputPath), { recursive: true });
90
+ const args = [
91
+ "--quality",
92
+ quality.includes("-") ? quality : `0-${quality}`,
93
+ "--speed",
94
+ "1",
95
+ "--force",
96
+ colors.toString(),
97
+ "--output",
98
+ outputPath,
99
+ inputPath
100
+ ];
101
+ try {
102
+ await execa(pngquant.default, args);
103
+ } catch (error) {
104
+ const execaError = error;
105
+ if (execaError.exitCode === 99) {
106
+ await copyFile(inputPath, outputPath);
107
+ } else {
108
+ throw error;
109
+ }
110
+ }
111
+ const compressedStats = await stat(outputPath);
112
+ const compressedSize = compressedStats.size;
113
+ const saved = originalSize - compressedSize;
114
+ return {
115
+ file: inputPath,
116
+ originalSize,
117
+ compressedSize,
118
+ saved,
119
+ savedPercent: saved / originalSize * 100
120
+ };
121
+ }
122
+ async function compressWithOptipng(inputPath, outputPath, options) {
123
+ const originalStats = await stat(inputPath);
124
+ const originalSize = originalStats.size;
125
+ const optipng = await import("optipng-bin");
126
+ const effort = parseInt(options.effort || "2");
127
+ await mkdir(dirname(outputPath), { recursive: true });
128
+ await copyFile(inputPath, outputPath);
129
+ const args = [
130
+ `-o${Math.min(7, effort)}`,
131
+ "-silent",
132
+ outputPath
133
+ ];
134
+ if (!options.keepMetadata) {
135
+ args.push("-strip", "all");
136
+ }
137
+ await execa(optipng.default, args);
138
+ const compressedStats = await stat(outputPath);
139
+ const compressedSize = compressedStats.size;
140
+ const saved = originalSize - compressedSize;
141
+ return {
142
+ file: inputPath,
143
+ originalSize,
144
+ compressedSize,
145
+ saved,
146
+ savedPercent: saved / originalSize * 100
147
+ };
148
+ }
149
+ async function compressWithGifsicle(inputPath, outputPath, options) {
150
+ const originalStats = await stat(inputPath);
151
+ const originalSize = originalStats.size;
152
+ const gifsicle = await import("gifsicle");
153
+ const colors = parseInt(options.colors || "256");
154
+ const effort = parseInt(options.effort || "3");
155
+ await mkdir(dirname(outputPath), { recursive: true });
156
+ const args = [
157
+ `-O${Math.min(3, effort)}`,
158
+ "--colors",
159
+ String(colors),
160
+ "-o",
161
+ outputPath,
162
+ inputPath
163
+ ];
164
+ if (options.lossless) {
165
+ args.splice(args.indexOf("--colors"), 2);
166
+ }
167
+ await execa(gifsicle.default, args);
168
+ const compressedStats = await stat(outputPath);
169
+ const compressedSize = compressedStats.size;
170
+ const saved = originalSize - compressedSize;
171
+ return {
172
+ file: inputPath,
173
+ originalSize,
174
+ compressedSize,
175
+ saved,
176
+ savedPercent: saved / originalSize * 100
177
+ };
178
+ }
179
+
180
+ // src/utils/compressor.ts
181
+ var SUPPORTED_FORMATS = [".jpg", ".jpeg", ".png", ".webp", ".avif", ".gif", ".tiff", ".svg"];
182
+ function isSupportedFormat(file) {
183
+ const ext = extname(file).toLowerCase();
184
+ return SUPPORTED_FORMATS.includes(ext);
185
+ }
186
+ function getFormat(file) {
187
+ const ext = extname(file).toLowerCase().slice(1);
188
+ return ext;
189
+ }
190
+ function parseResize(resize) {
191
+ if (resize.endsWith("%")) {
192
+ return { percent: parseInt(resize) / 100 };
193
+ }
194
+ const [w, h] = resize.split("x").map(Number);
195
+ return { width: w || void 0, height: h || void 0 };
196
+ }
197
+ async function compressSvg(inputPath, outputPath, options) {
198
+ const originalContent = await readFile2(inputPath, "utf-8");
199
+ const originalSize = Buffer.byteLength(originalContent, "utf-8");
200
+ const result = optimize(originalContent, {
201
+ multipass: true,
202
+ plugins: [
203
+ "preset-default",
204
+ "removeDimensions",
205
+ {
206
+ name: "removeAttrs",
207
+ params: {
208
+ attrs: options.keepMetadata ? [] : ["data-.*"]
209
+ }
210
+ }
211
+ ]
212
+ });
213
+ await mkdir2(dirname2(outputPath), { recursive: true });
214
+ await writeFile2(outputPath, result.data);
215
+ const compressedSize = Buffer.byteLength(result.data, "utf-8");
216
+ const saved = originalSize - compressedSize;
217
+ return {
218
+ file: inputPath,
219
+ originalSize,
220
+ compressedSize,
221
+ saved,
222
+ savedPercent: saved / originalSize * 100
223
+ };
224
+ }
225
+ async function needsResize(options) {
226
+ return !!(options.resize || options.maxWidth || options.maxHeight);
227
+ }
228
+ async function resizeWithSharp(inputPath, options) {
229
+ let image = sharp(inputPath);
230
+ const metadata = await image.metadata();
231
+ if (options.resize) {
232
+ const { width, height, percent } = parseResize(options.resize);
233
+ if (percent && metadata.width && metadata.height) {
234
+ image = image.resize(
235
+ Math.round(metadata.width * percent),
236
+ Math.round(metadata.height * percent)
237
+ );
238
+ } else if (width || height) {
239
+ image = image.resize(width, height, { fit: "inside", withoutEnlargement: true });
240
+ }
241
+ }
242
+ if (options.maxWidth || options.maxHeight) {
243
+ const maxW = options.maxWidth ? parseInt(options.maxWidth) : void 0;
244
+ const maxH = options.maxHeight ? parseInt(options.maxHeight) : void 0;
245
+ image = image.resize(maxW, maxH, { fit: "inside", withoutEnlargement: true });
246
+ }
247
+ return image.toBuffer();
248
+ }
249
+ async function compressJpeg(inputPath, outputPath, options) {
250
+ const engine = options.jpegEngine || "mozjpeg";
251
+ let actualInput = inputPath;
252
+ if (await needsResize(options)) {
253
+ const buffer = await resizeWithSharp(inputPath, options);
254
+ const tempPath = outputPath + ".tmp.jpg";
255
+ await mkdir2(dirname2(tempPath), { recursive: true });
256
+ await sharp(buffer).jpeg({ quality: 100 }).toFile(tempPath);
257
+ actualInput = tempPath;
258
+ }
259
+ let result;
260
+ switch (engine) {
261
+ case "mozjpeg":
262
+ result = await compressWithMozjpeg(actualInput, outputPath, options);
263
+ break;
264
+ case "jpegtran":
265
+ result = await compressWithJpegtran(actualInput, outputPath, options);
266
+ break;
267
+ case "sharp":
268
+ default:
269
+ result = await compressWithSharpJpeg(actualInput, outputPath, options);
270
+ break;
271
+ }
272
+ if (actualInput !== inputPath) {
273
+ await import("fs/promises").then((fs) => fs.unlink(actualInput).catch(() => {
274
+ }));
275
+ }
276
+ result.file = inputPath;
277
+ return result;
278
+ }
279
+ async function compressWithSharpJpeg(inputPath, outputPath, options) {
280
+ const originalStats = await stat2(inputPath);
281
+ const originalSize = originalStats.size;
282
+ const quality = parseInt(options.quality || "80");
283
+ await mkdir2(dirname2(outputPath), { recursive: true });
284
+ let image = sharp(inputPath);
285
+ if (!options.keepMetadata) {
286
+ image = image.rotate();
287
+ }
288
+ await image.jpeg({
289
+ quality: options.lossless ? 100 : quality,
290
+ progressive: options.progressive !== false,
291
+ mozjpeg: true
292
+ }).toFile(outputPath);
293
+ const compressedStats = await stat2(outputPath);
294
+ const compressedSize = compressedStats.size;
295
+ const saved = originalSize - compressedSize;
296
+ return {
297
+ file: inputPath,
298
+ originalSize,
299
+ compressedSize,
300
+ saved,
301
+ savedPercent: saved / originalSize * 100
302
+ };
303
+ }
304
+ async function compressPng(inputPath, outputPath, options) {
305
+ const engine = options.pngEngine || "pngquant";
306
+ let actualInput = inputPath;
307
+ if (await needsResize(options)) {
308
+ const buffer = await resizeWithSharp(inputPath, options);
309
+ const tempPath = outputPath + ".tmp.png";
310
+ await mkdir2(dirname2(tempPath), { recursive: true });
311
+ await sharp(buffer).png().toFile(tempPath);
312
+ actualInput = tempPath;
313
+ }
314
+ let result;
315
+ switch (engine) {
316
+ case "pngquant":
317
+ result = await compressWithPngquant(actualInput, outputPath, options);
318
+ break;
319
+ case "optipng":
320
+ result = await compressWithOptipng(actualInput, outputPath, options);
321
+ break;
322
+ case "sharp":
323
+ default:
324
+ result = await compressWithSharpPng(actualInput, outputPath, options);
325
+ break;
326
+ }
327
+ if (actualInput !== inputPath) {
328
+ await import("fs/promises").then((fs) => fs.unlink(actualInput).catch(() => {
329
+ }));
330
+ }
331
+ result.file = inputPath;
332
+ return result;
333
+ }
334
+ async function compressWithSharpPng(inputPath, outputPath, options) {
335
+ const originalStats = await stat2(inputPath);
336
+ const originalSize = originalStats.size;
337
+ const quality = parseInt(options.quality || "80");
338
+ const effort = parseInt(options.effort || "6");
339
+ await mkdir2(dirname2(outputPath), { recursive: true });
340
+ let image = sharp(inputPath);
341
+ if (!options.keepMetadata) {
342
+ image = image.rotate();
343
+ }
344
+ await image.png({
345
+ compressionLevel: Math.min(9, Math.round(effort * 0.9)),
346
+ palette: !options.lossless,
347
+ quality: options.lossless ? 100 : quality,
348
+ effort
349
+ }).toFile(outputPath);
350
+ const compressedStats = await stat2(outputPath);
351
+ const compressedSize = compressedStats.size;
352
+ const saved = originalSize - compressedSize;
353
+ return {
354
+ file: inputPath,
355
+ originalSize,
356
+ compressedSize,
357
+ saved,
358
+ savedPercent: saved / originalSize * 100
359
+ };
360
+ }
361
+ async function compressGif(inputPath, outputPath, options) {
362
+ const engine = options.gifEngine || "gifsicle";
363
+ if (engine === "gifsicle") {
364
+ return compressWithGifsicle(inputPath, outputPath, options);
365
+ }
366
+ return compressWithSharpGif(inputPath, outputPath, options);
367
+ }
368
+ async function compressWithSharpGif(inputPath, outputPath, options) {
369
+ const originalStats = await stat2(inputPath);
370
+ const originalSize = originalStats.size;
371
+ const effort = parseInt(options.effort || "6");
372
+ await mkdir2(dirname2(outputPath), { recursive: true });
373
+ await sharp(inputPath, { animated: true }).gif({ effort }).toFile(outputPath);
374
+ const compressedStats = await stat2(outputPath);
375
+ const compressedSize = compressedStats.size;
376
+ const saved = originalSize - compressedSize;
377
+ return {
378
+ file: inputPath,
379
+ originalSize,
380
+ compressedSize,
381
+ saved,
382
+ savedPercent: saved / originalSize * 100
383
+ };
384
+ }
385
+ async function compressRaster(inputPath, outputPath, options) {
386
+ const originalStats = await stat2(inputPath);
387
+ const originalSize = originalStats.size;
388
+ let image = sharp(inputPath);
389
+ const metadata = await image.metadata();
390
+ const quality = parseInt(options.quality || "80");
391
+ const effort = parseInt(options.effort || "6");
392
+ if (options.resize) {
393
+ const { width, height, percent } = parseResize(options.resize);
394
+ if (percent && metadata.width && metadata.height) {
395
+ image = image.resize(
396
+ Math.round(metadata.width * percent),
397
+ Math.round(metadata.height * percent)
398
+ );
399
+ } else if (width || height) {
400
+ image = image.resize(width, height, { fit: "inside", withoutEnlargement: true });
401
+ }
402
+ }
403
+ if (options.maxWidth || options.maxHeight) {
404
+ const maxW = options.maxWidth ? parseInt(options.maxWidth) : void 0;
405
+ const maxH = options.maxHeight ? parseInt(options.maxHeight) : void 0;
406
+ image = image.resize(maxW, maxH, { fit: "inside", withoutEnlargement: true });
407
+ }
408
+ if (!options.keepMetadata) {
409
+ image = image.rotate();
410
+ }
411
+ const inputFormat = getFormat(inputPath);
412
+ const outputFormat = options.format || (inputFormat === "jpeg" ? "jpg" : inputFormat);
413
+ await mkdir2(dirname2(outputPath), { recursive: true });
414
+ switch (outputFormat) {
415
+ case "webp":
416
+ await image.webp({
417
+ quality: options.lossless ? 100 : quality,
418
+ lossless: options.lossless || false,
419
+ effort
420
+ }).toFile(outputPath);
421
+ break;
422
+ case "avif":
423
+ await image.avif({
424
+ quality: options.lossless ? 100 : quality,
425
+ lossless: options.lossless || false,
426
+ effort
427
+ }).toFile(outputPath);
428
+ break;
429
+ case "tiff":
430
+ await image.tiff({
431
+ quality: options.lossless ? 100 : quality,
432
+ compression: options.lossless ? "lzw" : "jpeg"
433
+ }).toFile(outputPath);
434
+ break;
435
+ default:
436
+ await copyFile2(inputPath, outputPath);
437
+ }
438
+ const compressedStats = await stat2(outputPath);
439
+ const compressedSize = compressedStats.size;
440
+ const saved = originalSize - compressedSize;
441
+ return {
442
+ file: inputPath,
443
+ originalSize,
444
+ compressedSize,
445
+ saved,
446
+ savedPercent: saved / originalSize * 100
447
+ };
448
+ }
449
+ async function compressImage(inputPath, outputPath, options) {
450
+ const format = getFormat(inputPath);
451
+ let finalOutputPath = outputPath;
452
+ if (options.format && options.format !== format) {
453
+ const dir = dirname2(outputPath);
454
+ const name = basename(outputPath, extname(outputPath));
455
+ finalOutputPath = join2(dir, `${name}.${options.format}`);
456
+ }
457
+ switch (format) {
458
+ case "svg":
459
+ return compressSvg(inputPath, finalOutputPath, options);
460
+ case "jpg":
461
+ case "jpeg":
462
+ if (!options.format || options.format === "jpg") {
463
+ return compressJpeg(inputPath, finalOutputPath, options);
464
+ }
465
+ return compressRaster(inputPath, finalOutputPath, options);
466
+ case "png":
467
+ if (!options.format || options.format === "png") {
468
+ return compressPng(inputPath, finalOutputPath, options);
469
+ }
470
+ return compressRaster(inputPath, finalOutputPath, options);
471
+ case "gif":
472
+ if (!options.format) {
473
+ return compressGif(inputPath, finalOutputPath, options);
474
+ }
475
+ return compressRaster(inputPath, finalOutputPath, options);
476
+ default:
477
+ return compressRaster(inputPath, finalOutputPath, options);
478
+ }
479
+ }
480
+ function formatBytes(bytes) {
481
+ if (bytes === 0) return "0 B";
482
+ const k = 1024;
483
+ const sizes = ["B", "KB", "MB", "GB"];
484
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
485
+ return `${parseFloat((bytes / Math.pow(k, i)).toFixed(2))} ${sizes[i]}`;
486
+ }
487
+
488
+ // src/commands/compress.ts
489
+ async function findImages(input, recursive) {
490
+ const inputStat = await stat3(input);
491
+ if (inputStat.isFile()) {
492
+ return isSupportedFormat(input) ? [input] : [];
493
+ }
494
+ const pattern = recursive ? "**/*.{jpg,jpeg,png,webp,avif,gif,tiff,svg,JPG,JPEG,PNG,WEBP,AVIF,GIF,TIFF,SVG}" : "*.{jpg,jpeg,png,webp,avif,gif,tiff,svg,JPG,JPEG,PNG,WEBP,AVIF,GIF,TIFF,SVG}";
495
+ return glob(pattern, { cwd: input, absolute: true, nodir: true });
496
+ }
497
+ function getOutputPath(inputFile, inputDir, outputDir, inPlace) {
498
+ if (inPlace) {
499
+ return inputFile;
500
+ }
501
+ const relativePath = relative(inputDir, inputFile);
502
+ return join3(outputDir, relativePath);
503
+ }
504
+ async function compress(input, options) {
505
+ const inputPath = resolve(input);
506
+ const inputStat = await stat3(inputPath).catch(() => null);
507
+ if (!inputStat) {
508
+ console.error(pc.red(`Error: Path not found: ${inputPath}`));
509
+ process.exit(1);
510
+ }
511
+ const inputDir = inputStat.isDirectory() ? inputPath : dirname3(inputPath);
512
+ const outputDir = options.inPlace ? inputDir : options.output ? resolve(options.output) : join3(inputDir, "Kasai");
513
+ const spinner = ora("Finding images...").start();
514
+ const images = await findImages(inputPath, options.recursive || false);
515
+ if (images.length === 0) {
516
+ spinner.fail("No supported images found");
517
+ return;
518
+ }
519
+ spinner.succeed(`Found ${images.length} image${images.length > 1 ? "s" : ""}`);
520
+ if (!options.inPlace) {
521
+ await mkdir3(outputDir, { recursive: true });
522
+ }
523
+ const results = [];
524
+ let totalOriginal = 0;
525
+ let totalCompressed = 0;
526
+ for (let i = 0; i < images.length; i++) {
527
+ const image = images[i];
528
+ const outputPath = getOutputPath(image, inputDir, outputDir, options.inPlace || false);
529
+ const fileName = basename2(image);
530
+ const progress = pc.dim(`[${i + 1}/${images.length}]`);
531
+ const compressSpinner = ora(`${progress} Compressing ${fileName}...`).start();
532
+ try {
533
+ const result = await compressImage(image, outputPath, options);
534
+ results.push(result);
535
+ totalOriginal += result.originalSize;
536
+ totalCompressed += result.compressedSize;
537
+ const savedStr = result.saved > 0 ? pc.green(`-${result.savedPercent.toFixed(1)}%`) : pc.yellow(`+${Math.abs(result.savedPercent).toFixed(1)}%`);
538
+ compressSpinner.succeed(
539
+ `${progress} ${fileName} ${pc.dim(formatBytes(result.originalSize))} \u2192 ${pc.dim(formatBytes(result.compressedSize))} ${savedStr}`
540
+ );
541
+ } catch (error) {
542
+ const errorMessage = error instanceof Error ? error.message : "Unknown error";
543
+ compressSpinner.fail(`${progress} ${fileName} - ${pc.red(errorMessage)}`);
544
+ }
545
+ }
546
+ console.log("");
547
+ const totalSaved = totalOriginal - totalCompressed;
548
+ const totalPercent = totalOriginal > 0 ? totalSaved / totalOriginal * 100 : 0;
549
+ console.log(pc.bold("Summary:"));
550
+ console.log(` Files processed: ${pc.cyan(results.length.toString())}`);
551
+ console.log(` Original size: ${pc.dim(formatBytes(totalOriginal))}`);
552
+ console.log(` Compressed size: ${pc.dim(formatBytes(totalCompressed))}`);
553
+ console.log(
554
+ ` Total saved: ${totalSaved > 0 ? pc.green(`${formatBytes(totalSaved)} (${totalPercent.toFixed(1)}%)`) : pc.yellow("0 B")}`
555
+ );
556
+ if (!options.inPlace) {
557
+ console.log(` Output: ${pc.cyan(outputDir)}`);
558
+ }
559
+ }
560
+
561
+ // src/commands/interactive.ts
562
+ import * as p from "@clack/prompts";
563
+ import pc2 from "picocolors";
564
+ import { resolve as resolve2 } from "path";
565
+ import { statSync } from "fs";
566
+ async function interactive() {
567
+ p.intro(pc2.bgCyan(pc2.black(" QASAI Image Compression ")));
568
+ const input = await p.text({
569
+ message: "Enter the path to compress",
570
+ placeholder: "./images or ./photo.jpg",
571
+ defaultValue: ".",
572
+ validate: (value) => {
573
+ try {
574
+ const path = resolve2(value);
575
+ statSync(path);
576
+ return void 0;
577
+ } catch {
578
+ return "Path does not exist";
579
+ }
580
+ }
581
+ });
582
+ if (p.isCancel(input)) {
583
+ p.cancel("Operation cancelled");
584
+ process.exit(0);
585
+ }
586
+ const outputMode = await p.select({
587
+ message: "Where should compressed images be saved?",
588
+ options: [
589
+ { value: "kasai", label: "Kasai folder", hint: "Creates a Kasai folder in the input directory" },
590
+ { value: "custom", label: "Custom folder", hint: "Specify a custom output directory" },
591
+ { value: "inplace", label: "In place", hint: "Overwrites original files (destructive)" }
592
+ ]
593
+ });
594
+ if (p.isCancel(outputMode)) {
595
+ p.cancel("Operation cancelled");
596
+ process.exit(0);
597
+ }
598
+ let output;
599
+ let inPlace = false;
600
+ if (outputMode === "custom") {
601
+ const customOutput = await p.text({
602
+ message: "Enter output directory path",
603
+ placeholder: "./compressed"
604
+ });
605
+ if (p.isCancel(customOutput)) {
606
+ p.cancel("Operation cancelled");
607
+ process.exit(0);
608
+ }
609
+ output = customOutput;
610
+ } else if (outputMode === "inplace") {
611
+ const confirm2 = await p.confirm({
612
+ message: "This will overwrite original files. Are you sure?",
613
+ initialValue: false
614
+ });
615
+ if (p.isCancel(confirm2) || !confirm2) {
616
+ p.cancel("Operation cancelled");
617
+ process.exit(0);
618
+ }
619
+ inPlace = true;
620
+ }
621
+ const compressionType = await p.select({
622
+ message: "Compression type",
623
+ options: [
624
+ { value: "lossy", label: "Lossy", hint: "Smaller files, slight quality loss (recommended)" },
625
+ { value: "lossless", label: "Lossless", hint: "No quality loss, larger files" }
626
+ ]
627
+ });
628
+ if (p.isCancel(compressionType)) {
629
+ p.cancel("Operation cancelled");
630
+ process.exit(0);
631
+ }
632
+ let quality = "80";
633
+ if (compressionType === "lossy") {
634
+ const qualityInput = await p.text({
635
+ message: "Quality level (1-100)",
636
+ placeholder: "80",
637
+ defaultValue: "80",
638
+ validate: (value) => {
639
+ const num = parseInt(value);
640
+ if (isNaN(num) || num < 1 || num > 100) {
641
+ return "Please enter a number between 1 and 100";
642
+ }
643
+ return void 0;
644
+ }
645
+ });
646
+ if (p.isCancel(qualityInput)) {
647
+ p.cancel("Operation cancelled");
648
+ process.exit(0);
649
+ }
650
+ quality = qualityInput;
651
+ }
652
+ const engineConfig = await p.confirm({
653
+ message: "Configure compression engines? (defaults are optimal)",
654
+ initialValue: false
655
+ });
656
+ if (p.isCancel(engineConfig)) {
657
+ p.cancel("Operation cancelled");
658
+ process.exit(0);
659
+ }
660
+ let jpegEngine = "mozjpeg";
661
+ let pngEngine = "pngquant";
662
+ let gifEngine = "gifsicle";
663
+ if (engineConfig) {
664
+ const jpegEngineChoice = await p.select({
665
+ message: "JPEG compression engine",
666
+ options: [
667
+ { value: "mozjpeg", label: "MozJPEG", hint: "Best compression, recommended" },
668
+ { value: "jpegtran", label: "jpegtran", hint: "Lossless optimization only" },
669
+ { value: "sharp", label: "Sharp", hint: "Fast, good compression" }
670
+ ]
671
+ });
672
+ if (p.isCancel(jpegEngineChoice)) {
673
+ p.cancel("Operation cancelled");
674
+ process.exit(0);
675
+ }
676
+ jpegEngine = jpegEngineChoice;
677
+ const pngEngineChoice = await p.select({
678
+ message: "PNG compression engine",
679
+ options: [
680
+ { value: "pngquant", label: "pngquant", hint: "Best lossy compression, recommended" },
681
+ { value: "optipng", label: "OptiPNG", hint: "Lossless optimization" },
682
+ { value: "sharp", label: "Sharp", hint: "Fast, good compression" }
683
+ ]
684
+ });
685
+ if (p.isCancel(pngEngineChoice)) {
686
+ p.cancel("Operation cancelled");
687
+ process.exit(0);
688
+ }
689
+ pngEngine = pngEngineChoice;
690
+ const gifEngineChoice = await p.select({
691
+ message: "GIF compression engine",
692
+ options: [
693
+ { value: "gifsicle", label: "Gifsicle", hint: "Best for animated GIFs, recommended" },
694
+ { value: "sharp", label: "Sharp", hint: "Fast, basic compression" }
695
+ ]
696
+ });
697
+ if (p.isCancel(gifEngineChoice)) {
698
+ p.cancel("Operation cancelled");
699
+ process.exit(0);
700
+ }
701
+ gifEngine = gifEngineChoice;
702
+ }
703
+ const advancedOptions = await p.multiselect({
704
+ message: "Additional options",
705
+ options: [
706
+ { value: "recursive", label: "Process subdirectories", hint: "Include images in subfolders" },
707
+ { value: "keepMetadata", label: "Keep metadata", hint: "Preserve EXIF and other metadata" },
708
+ { value: "resize", label: "Resize images", hint: "Set maximum dimensions" }
709
+ ],
710
+ required: false
711
+ });
712
+ if (p.isCancel(advancedOptions)) {
713
+ p.cancel("Operation cancelled");
714
+ process.exit(0);
715
+ }
716
+ const options = {
717
+ output,
718
+ inPlace,
719
+ quality,
720
+ lossless: compressionType === "lossless",
721
+ recursive: advancedOptions.includes("recursive"),
722
+ keepMetadata: advancedOptions.includes("keepMetadata"),
723
+ progressive: true,
724
+ jpegEngine,
725
+ pngEngine,
726
+ gifEngine
727
+ };
728
+ if (advancedOptions.includes("resize")) {
729
+ const resizeType = await p.select({
730
+ message: "Resize method",
731
+ options: [
732
+ { value: "maxWidth", label: "Maximum width", hint: "Maintains aspect ratio" },
733
+ { value: "maxHeight", label: "Maximum height", hint: "Maintains aspect ratio" },
734
+ { value: "dimensions", label: "Specific dimensions", hint: "e.g., 800x600" },
735
+ { value: "percent", label: "Percentage", hint: "e.g., 50%" }
736
+ ]
737
+ });
738
+ if (p.isCancel(resizeType)) {
739
+ p.cancel("Operation cancelled");
740
+ process.exit(0);
741
+ }
742
+ if (resizeType === "maxWidth") {
743
+ const maxWidth = await p.text({
744
+ message: "Maximum width in pixels",
745
+ placeholder: "1920",
746
+ validate: (value) => {
747
+ const num = parseInt(value);
748
+ if (isNaN(num) || num < 1) {
749
+ return "Please enter a valid number";
750
+ }
751
+ return void 0;
752
+ }
753
+ });
754
+ if (p.isCancel(maxWidth)) {
755
+ p.cancel("Operation cancelled");
756
+ process.exit(0);
757
+ }
758
+ options.maxWidth = maxWidth;
759
+ } else if (resizeType === "maxHeight") {
760
+ const maxHeight = await p.text({
761
+ message: "Maximum height in pixels",
762
+ placeholder: "1080",
763
+ validate: (value) => {
764
+ const num = parseInt(value);
765
+ if (isNaN(num) || num < 1) {
766
+ return "Please enter a valid number";
767
+ }
768
+ return void 0;
769
+ }
770
+ });
771
+ if (p.isCancel(maxHeight)) {
772
+ p.cancel("Operation cancelled");
773
+ process.exit(0);
774
+ }
775
+ options.maxHeight = maxHeight;
776
+ } else if (resizeType === "dimensions") {
777
+ const dimensions = await p.text({
778
+ message: "Dimensions (WIDTHxHEIGHT)",
779
+ placeholder: "800x600",
780
+ validate: (value) => {
781
+ if (!/^\d+x\d+$/.test(value)) {
782
+ return "Please use format: WIDTHxHEIGHT (e.g., 800x600)";
783
+ }
784
+ return void 0;
785
+ }
786
+ });
787
+ if (p.isCancel(dimensions)) {
788
+ p.cancel("Operation cancelled");
789
+ process.exit(0);
790
+ }
791
+ options.resize = dimensions;
792
+ } else if (resizeType === "percent") {
793
+ const percent = await p.text({
794
+ message: "Resize percentage",
795
+ placeholder: "50",
796
+ validate: (value) => {
797
+ const num = parseInt(value);
798
+ if (isNaN(num) || num < 1 || num > 100) {
799
+ return "Please enter a number between 1 and 100";
800
+ }
801
+ return void 0;
802
+ }
803
+ });
804
+ if (p.isCancel(percent)) {
805
+ p.cancel("Operation cancelled");
806
+ process.exit(0);
807
+ }
808
+ options.resize = `${percent}%`;
809
+ }
810
+ }
811
+ const convertFormat = await p.confirm({
812
+ message: "Convert images to a different format?",
813
+ initialValue: false
814
+ });
815
+ if (p.isCancel(convertFormat)) {
816
+ p.cancel("Operation cancelled");
817
+ process.exit(0);
818
+ }
819
+ if (convertFormat) {
820
+ const format = await p.select({
821
+ message: "Convert to format",
822
+ options: [
823
+ { value: "webp", label: "WebP", hint: "Modern format, great compression" },
824
+ { value: "avif", label: "AVIF", hint: "Best compression, newer format" },
825
+ { value: "jpg", label: "JPEG", hint: "Universal compatibility" },
826
+ { value: "png", label: "PNG", hint: "Lossless, supports transparency" }
827
+ ]
828
+ });
829
+ if (p.isCancel(format)) {
830
+ p.cancel("Operation cancelled");
831
+ process.exit(0);
832
+ }
833
+ options.format = format;
834
+ }
835
+ console.log("");
836
+ p.outro(pc2.dim("Starting compression..."));
837
+ console.log("");
838
+ await compress(input, options);
839
+ }
840
+
841
+ // src/utils/banner.ts
842
+ import pc3 from "picocolors";
843
+ function banner() {
844
+ const terminalWidth = process.stdout.columns || 80;
845
+ const isSmall = terminalWidth < 60;
846
+ const bigArt = `
847
+ ${pc3.white(" \u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2557")}
848
+ ${pc3.white(" \u2588\u2588\u2554\u2550\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2551")}
849
+ ${pc3.gray(" \u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2551\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2551\u2588\u2588\u2551")}
850
+ ${pc3.gray(" \u2588\u2588\u2551\u2584\u2584 \u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2551\u255A\u2550\u2550\u2550\u2550\u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2551\u2588\u2588\u2551")}
851
+ ${pc3.dim(" \u255A\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2551")}
852
+ ${pc3.dim(" \u255A\u2550\u2550\u2580\u2580\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u255D\u255A\u2550\u2550\u2550\u2550\u2550\u2550\u255D\u255A\u2550\u255D \u255A\u2550\u255D\u255A\u2550\u255D")}
853
+ `;
854
+ const smallArt = `
855
+ ${pc3.white("\u2588\u2580\u2588\u2584\u2580\u2588\u2588\u2580\u2584\u2580\u2588\u2588")}
856
+ ${pc3.gray("\u2580\u2580\u2588\u2588\u2580\u2588\u2584\u2588\u2588\u2580\u2588\u2588")}
857
+ `;
858
+ console.log(isSmall ? smallArt : bigArt);
859
+ console.log(pc3.dim(" Image Compression CLI\n"));
860
+ }
861
+
862
+ // src/index.ts
863
+ var require2 = createRequire(import.meta.url);
864
+ var pkg = require2("../package.json");
865
+ var program = new Command();
866
+ program.name("qasai").description("Image compression CLI with lossless and lossy options").version(pkg.version, "-v, --version").helpOption("-h, --help", "Display help information");
867
+ program.command("compress").description("Compress images in a directory").argument("[input]", "Input directory or file", ".").option("-o, --output <dir>", "Output directory (default: Kasai folder in input dir)").option("-i, --in-place", "Compress images in place (overwrites originals)").option("-q, --quality <number>", "Quality level 1-100 (default: 80)", "80").option("-l, --lossless", "Use lossless compression").option("--resize <dimensions>", "Resize images (e.g., 800x600, 50%)").option("--max-width <pixels>", "Maximum width (maintains aspect ratio)").option("--max-height <pixels>", "Maximum height (maintains aspect ratio)").option("-f, --format <format>", "Convert to format (jpg, png, webp, avif)").option("-r, --recursive", "Process subdirectories recursively").option("--keep-metadata", "Preserve image metadata (EXIF, etc.)").option("--no-progressive", "Disable progressive encoding for JPEGs").addOption(new Option("--effort <level>", "Compression effort 1-10 (higher = slower but smaller)").default("6")).addOption(
868
+ new Option("--jpeg-engine <engine>", "JPEG compression engine").choices(["mozjpeg", "jpegtran", "sharp"]).default("mozjpeg")
869
+ ).addOption(
870
+ new Option("--png-engine <engine>", "PNG compression engine").choices(["pngquant", "optipng", "sharp"]).default("pngquant")
871
+ ).addOption(
872
+ new Option("--gif-engine <engine>", "GIF compression engine").choices(["gifsicle", "sharp"]).default("gifsicle")
873
+ ).option("--png-quality <range>", "PNG quality range for pngquant (e.g., 65-80)", "65-80").option("--colors <number>", "Max colors for PNG/GIF (2-256)", "256").action(compress);
874
+ program.command("interactive", { isDefault: true }).description("Run in interactive mode").action(async () => {
875
+ banner();
876
+ await interactive();
877
+ });
878
+ program.parse();
879
+ //# sourceMappingURL=index.js.map