@spark-apps/piclet 1.0.3 → 1.0.5

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/cli.js CHANGED
@@ -40,14 +40,15 @@ ${renderLogo()}`);
40
40
  import chalk2 from "chalk";
41
41
 
42
42
  // src/tools/border.ts
43
- import { existsSync as existsSync3, readFileSync as readFileSync2 } from "fs";
43
+ import { existsSync as existsSync5, readFileSync as readFileSync3 } from "fs";
44
44
  import { tmpdir } from "os";
45
- import { basename as basename2, join as join3 } from "path";
45
+ import { basename as basename2, join as join4 } from "path";
46
46
 
47
47
  // src/lib/gui-server.ts
48
48
  import { spawn } from "child_process";
49
+ import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
49
50
  import { createServer } from "http";
50
- import { dirname as dirname2, join as join2 } from "path";
51
+ import { dirname as dirname2, extname, join as join2 } from "path";
51
52
  import { fileURLToPath } from "url";
52
53
  import express from "express";
53
54
 
@@ -340,6 +341,58 @@ function startGuiServer(options) {
340
341
  res.json({ success: false, error: err.message });
341
342
  }
342
343
  });
344
+ app.post("/api/simplify-gif", async (req, res) => {
345
+ if (!options.onSimplifyGif) {
346
+ res.json({ success: false, error: "GIF simplification not supported" });
347
+ return;
348
+ }
349
+ try {
350
+ const skipFactor = req.body.skipFactor ?? 2;
351
+ const result = await options.onSimplifyGif(skipFactor);
352
+ if (result.success) {
353
+ options.imageInfo.filePath = result.filePath;
354
+ options.imageInfo.fileName = result.fileName;
355
+ options.imageInfo.width = result.width;
356
+ options.imageInfo.height = result.height;
357
+ options.imageInfo.frameCount = result.frameCount;
358
+ }
359
+ res.json(result);
360
+ } catch (err) {
361
+ res.json({ success: false, error: err.message });
362
+ }
363
+ });
364
+ app.post("/api/delete-frame", async (req, res) => {
365
+ if (!options.onDeleteFrame) {
366
+ res.json({ success: false, error: "Frame deletion not supported" });
367
+ return;
368
+ }
369
+ try {
370
+ const frameIndex = req.body.frameIndex;
371
+ const result = await options.onDeleteFrame(frameIndex);
372
+ if (result.success) {
373
+ options.imageInfo.frameCount = result.frameCount;
374
+ }
375
+ res.json(result);
376
+ } catch (err) {
377
+ res.json({ success: false, error: err.message });
378
+ }
379
+ });
380
+ app.post("/api/replace-frame", async (req, res) => {
381
+ if (!options.onReplaceFrame) {
382
+ res.json({ success: false, error: "Frame replacement not supported" });
383
+ return;
384
+ }
385
+ try {
386
+ const { frameIndex, imageData } = req.body;
387
+ const result = await options.onReplaceFrame(frameIndex, imageData);
388
+ if (result.success) {
389
+ options.imageInfo.frameCount = result.frameCount;
390
+ }
391
+ res.json(result);
392
+ } catch (err) {
393
+ res.json({ success: false, error: err.message });
394
+ }
395
+ });
343
396
  app.post("/api/preview", async (req, res) => {
344
397
  if (!options.onPreview) {
345
398
  res.json({ success: false, error: "Preview not supported" });
@@ -359,6 +412,11 @@ function startGuiServer(options) {
359
412
  try {
360
413
  const result = await options.onProcess(req.body);
361
414
  processResult = result.success;
415
+ if (result.outputPath) {
416
+ options.lastOutputPath = result.outputPath;
417
+ } else {
418
+ options.lastOutputPath = void 0;
419
+ }
362
420
  res.json(result);
363
421
  } catch (err) {
364
422
  processResult = false;
@@ -436,8 +494,39 @@ function startGuiServer(options) {
436
494
  }).unref();
437
495
  res.json({ success: true });
438
496
  });
497
+ app.get("/api/output-preview", (_req, res) => {
498
+ const outputPath = options.lastOutputPath;
499
+ if (!outputPath) {
500
+ res.json({ success: false, error: "No output file" });
501
+ return;
502
+ }
503
+ try {
504
+ if (!existsSync2(outputPath)) {
505
+ res.json({ success: false, error: "Output file not found" });
506
+ return;
507
+ }
508
+ const buffer = readFileSync2(outputPath);
509
+ const ext = extname(outputPath).toLowerCase();
510
+ const mimeTypes = {
511
+ ".png": "image/png",
512
+ ".jpg": "image/jpeg",
513
+ ".jpeg": "image/jpeg",
514
+ ".gif": "image/gif",
515
+ ".ico": "image/x-icon"
516
+ };
517
+ const mimeType = mimeTypes[ext] || "image/png";
518
+ res.json({
519
+ success: true,
520
+ imageData: `data:${mimeType};base64,${buffer.toString("base64")}`,
521
+ isGif: ext === ".gif"
522
+ });
523
+ } catch (err) {
524
+ res.json({ success: false, error: err.message });
525
+ }
526
+ });
439
527
  app.post("/api/open-folder", (_req, res) => {
440
- const filePath = options.imageInfo.filePath;
528
+ const outputPath = options.lastOutputPath;
529
+ const filePath = outputPath || options.imageInfo.filePath;
441
530
  let winPath = filePath;
442
531
  const wslMatch = filePath.match(/^\/mnt\/([a-z])\/(.*)$/);
443
532
  if (wslMatch) {
@@ -445,8 +534,10 @@ function startGuiServer(options) {
445
534
  const rest = wslMatch[2].replace(/\//g, "\\");
446
535
  winPath = `${drive}:\\${rest}`;
447
536
  }
448
- const dir = winPath.substring(0, winPath.lastIndexOf("\\")) || winPath.substring(0, winPath.lastIndexOf("/"));
449
- spawn("powershell.exe", ["-WindowStyle", "Hidden", "-Command", `explorer.exe "${dir}"`], {
537
+ const lastSep = Math.max(winPath.lastIndexOf("\\"), winPath.lastIndexOf("/"));
538
+ const dir = lastSep > 0 ? winPath.substring(0, lastSep) : winPath;
539
+ const explorerCmd = outputPath ? `explorer.exe /select,"${winPath}"` : `explorer.exe "${dir}"`;
540
+ spawn("powershell.exe", ["-WindowStyle", "Hidden", "-Command", explorerCmd], {
450
541
  detached: true,
451
542
  stdio: "ignore",
452
543
  windowsHide: true
@@ -534,7 +625,7 @@ function clearLine() {
534
625
 
535
626
  // src/lib/magick.ts
536
627
  import { exec } from "child_process";
537
- import { copyFileSync, existsSync as existsSync2, mkdirSync as mkdirSync2, unlinkSync } from "fs";
628
+ import { copyFileSync, existsSync as existsSync3, mkdirSync as mkdirSync2, unlinkSync } from "fs";
538
629
  import { dirname as dirname3 } from "path";
539
630
  import { promisify } from "util";
540
631
  var execAsync = promisify(exec);
@@ -549,7 +640,7 @@ function isGif(imagePath) {
549
640
  return imagePath.toLowerCase().endsWith(".gif");
550
641
  }
551
642
  function getGifOutputSuffix(outputPath) {
552
- return isGif(outputPath) ? " -layers Optimize" : "";
643
+ return isGif(outputPath) ? " -dispose Background -layers OptimizePlus" : "";
553
644
  }
554
645
  function getCoalescePrefix(inputPath) {
555
646
  return isGif(inputPath) ? "-coalesce " : "";
@@ -688,6 +779,20 @@ async function removeBackgroundBorderOnly(inputPath, outputPath, color, fuzz) {
688
779
  return false;
689
780
  }
690
781
  }
782
+ async function removeBackgroundEdgeAware(inputPath, outputPath, color, fuzz, featherAmount = 50) {
783
+ if (isGif(inputPath)) {
784
+ return removeBackgroundBorderOnly(inputPath, outputPath, color, fuzz);
785
+ }
786
+ try {
787
+ const featherRadius = 0.5 + featherAmount / 100 * 2.5;
788
+ await execAsync(
789
+ `convert "${inputPath}" -bordercolor "${color}" -border 1x1 -fill none -fuzz ${fuzz}% -draw "matte 0,0 floodfill" -shave 1x1 \\( +clone -alpha extract -blur 0x${featherRadius} \\) -compose CopyOpacity -composite "${outputPath}"`
790
+ );
791
+ return true;
792
+ } catch {
793
+ return removeBackgroundBorderOnly(inputPath, outputPath, color, fuzz);
794
+ }
795
+ }
691
796
  async function createIco(inputPath, outputPath, sizes = [256, 128, 64, 48, 32, 16]) {
692
797
  try {
693
798
  const sizeStr = sizes.join(",");
@@ -710,14 +815,14 @@ async function createIcoFromMultiple(pngPaths, outputPath) {
710
815
  }
711
816
  function ensureDir(filePath) {
712
817
  const dir = dirname3(filePath);
713
- if (!existsSync2(dir)) {
818
+ if (!existsSync3(dir)) {
714
819
  mkdirSync2(dir, { recursive: true });
715
820
  }
716
821
  }
717
822
  function cleanup(...files) {
718
823
  for (const file of files) {
719
824
  try {
720
- if (existsSync2(file)) {
825
+ if (existsSync3(file)) {
721
826
  unlinkSync(file);
722
827
  }
723
828
  } catch {
@@ -736,7 +841,13 @@ async function extractFirstFrame(inputPath, outputPath, frameIndex = 0) {
736
841
  }
737
842
  try {
738
843
  if (lowerPath.endsWith(".gif")) {
739
- await execAsync(`convert "${inputPath}" -coalesce miff:- | convert "miff:-[${frameIndex}]" "${outputPath}"`);
844
+ if (frameIndex === 0) {
845
+ await execAsync(`convert "${inputPath}[0]" "${outputPath}"`);
846
+ } else {
847
+ await execAsync(
848
+ `convert "${inputPath}[0-${frameIndex}]" -coalesce -delete 0--2 "${outputPath}"`
849
+ );
850
+ }
740
851
  } else {
741
852
  await execAsync(`convert "${inputPath}[${frameIndex}]" "${outputPath}"`);
742
853
  }
@@ -764,7 +875,7 @@ async function extractAllFrames(inputPath, outputDir, baseName) {
764
875
  try {
765
876
  ensureDir(`${outputDir}/dummy`);
766
877
  await execAsync(
767
- `convert "${inputPath}" -coalesce "${outputDir}/${baseName}-%04d.png"`
878
+ `convert "${inputPath}" -coalesce +adjoin "${outputDir}/${baseName}-%04d.png"`
768
879
  );
769
880
  const { stdout } = await execAsync(`ls -1 "${outputDir}/${baseName}"-*.png 2>/dev/null || true`);
770
881
  const files = stdout.trim().split("\n").filter((f) => f.length > 0);
@@ -893,9 +1004,97 @@ async function replaceColor(inputPath, outputPath, fromColor, toColor, fuzz) {
893
1004
  return false;
894
1005
  }
895
1006
  }
1007
+ async function deleteGifFrame(inputPath, outputPath, frameIndex) {
1008
+ try {
1009
+ const originalCount = await getFrameCount(inputPath);
1010
+ if (originalCount <= 1) {
1011
+ return { success: false };
1012
+ }
1013
+ if (frameIndex < 0 || frameIndex >= originalCount) {
1014
+ return { success: false };
1015
+ }
1016
+ await execAsync(
1017
+ `convert "${inputPath}" -coalesce -delete ${frameIndex} -dispose Background -layers OptimizePlus "${outputPath}"`
1018
+ );
1019
+ const newCount = await getFrameCount(outputPath);
1020
+ return { success: true, frameCount: newCount };
1021
+ } catch {
1022
+ return { success: false };
1023
+ }
1024
+ }
1025
+ async function replaceGifFrame(inputPath, outputPath, frameIndex, replacementPath) {
1026
+ try {
1027
+ const frameCount = await getFrameCount(inputPath);
1028
+ if (frameIndex < 0 || frameIndex >= frameCount) {
1029
+ return { success: false };
1030
+ }
1031
+ const dims = await getDimensions(inputPath);
1032
+ if (!dims) return { success: false };
1033
+ const tempReplacement = `${outputPath}.tmp.png`;
1034
+ await execAsync(
1035
+ `convert "${replacementPath}" -resize ${dims[0]}x${dims[1]}^ -gravity center -extent ${dims[0]}x${dims[1]} "${tempReplacement}"`
1036
+ );
1037
+ if (frameIndex === 0) {
1038
+ await execAsync(
1039
+ `convert "${tempReplacement}" \\( "${inputPath}" -coalesce \\) -delete 1 -dispose Background -layers OptimizePlus "${outputPath}"`
1040
+ );
1041
+ } else if (frameIndex === frameCount - 1) {
1042
+ await execAsync(
1043
+ `convert \\( "${inputPath}" -coalesce -delete -1 \\) "${tempReplacement}" -dispose Background -layers OptimizePlus "${outputPath}"`
1044
+ );
1045
+ } else {
1046
+ await execAsync(
1047
+ `convert \\( "${inputPath}" -coalesce \\) -delete ${frameIndex} "${outputPath}.frames.gif" && convert "${outputPath}.frames.gif[0-${frameIndex - 1}]" "${tempReplacement}" "${outputPath}.frames.gif[${frameIndex}-]" -dispose Background -layers OptimizePlus "${outputPath}" && rm -f "${outputPath}.frames.gif"`
1048
+ );
1049
+ }
1050
+ try {
1051
+ unlinkSync(tempReplacement);
1052
+ } catch {
1053
+ }
1054
+ const newCount = await getFrameCount(outputPath);
1055
+ return { success: true, frameCount: newCount };
1056
+ } catch {
1057
+ return { success: false };
1058
+ }
1059
+ }
1060
+ async function simplifyGif(inputPath, outputPath, skipFactor) {
1061
+ if (skipFactor < 2) {
1062
+ if (inputPath !== outputPath) {
1063
+ try {
1064
+ copyFileSync(inputPath, outputPath);
1065
+ const count2 = await getFrameCount(outputPath);
1066
+ return { success: true, frameCount: count2 };
1067
+ } catch {
1068
+ return { success: false };
1069
+ }
1070
+ }
1071
+ const count = await getFrameCount(inputPath);
1072
+ return { success: true, frameCount: count };
1073
+ }
1074
+ try {
1075
+ const originalCount = await getFrameCount(inputPath);
1076
+ if (originalCount <= 1) {
1077
+ copyFileSync(inputPath, outputPath);
1078
+ return { success: true, frameCount: 1 };
1079
+ }
1080
+ const frameIndices = [];
1081
+ for (let i = 0; i < originalCount; i += skipFactor) {
1082
+ frameIndices.push(i);
1083
+ }
1084
+ const keepPattern = frameIndices.join(",");
1085
+ await execAsync(
1086
+ `convert "${inputPath}[${keepPattern}]" -coalesce -dispose Background -layers OptimizePlus "${outputPath}"`
1087
+ );
1088
+ const newCount = await getFrameCount(outputPath);
1089
+ return { success: true, frameCount: newCount };
1090
+ } catch {
1091
+ return { success: false };
1092
+ }
1093
+ }
896
1094
 
897
1095
  // src/lib/paths.ts
898
- import { basename, dirname as dirname4, extname, resolve } from "path";
1096
+ import { existsSync as existsSync4, mkdirSync as mkdirSync3 } from "fs";
1097
+ import { basename, dirname as dirname4, extname as extname2, join as join3, resolve } from "path";
899
1098
  function windowsToWsl(winPath) {
900
1099
  if (winPath.startsWith("/mnt/")) {
901
1100
  return winPath;
@@ -926,7 +1125,7 @@ function normalizePath(inputPath) {
926
1125
  function getFileInfo(filePath) {
927
1126
  const dir = dirname4(filePath);
928
1127
  const base = basename(filePath);
929
- const ext = extname(filePath);
1128
+ const ext = extname2(filePath);
930
1129
  const name = base.slice(0, -ext.length);
931
1130
  return {
932
1131
  dirname: dir,
@@ -935,6 +1134,20 @@ function getFileInfo(filePath) {
935
1134
  extension: ext
936
1135
  };
937
1136
  }
1137
+ function getOutputDir(inputPath) {
1138
+ const dir = dirname4(inputPath);
1139
+ if (basename(dir) === "PicLet") {
1140
+ return dir;
1141
+ }
1142
+ return join3(dir, "PicLet");
1143
+ }
1144
+ function ensureOutputDir(inputPath) {
1145
+ const outDir = getOutputDir(inputPath);
1146
+ if (!existsSync4(outDir)) {
1147
+ mkdirSync3(outDir, { recursive: true });
1148
+ }
1149
+ return outDir;
1150
+ }
938
1151
 
939
1152
  // src/lib/prompts.ts
940
1153
  import prompts from "prompts";
@@ -1083,7 +1296,7 @@ async function run(inputRaw) {
1083
1296
  return false;
1084
1297
  }
1085
1298
  const input = normalizePath(inputRaw);
1086
- if (!existsSync3(input)) {
1299
+ if (!existsSync5(input)) {
1087
1300
  error(`File not found: ${input}`);
1088
1301
  await pauseOnError();
1089
1302
  return false;
@@ -1102,7 +1315,7 @@ async function run(inputRaw) {
1102
1315
  }
1103
1316
  async function runGUI(inputRaw) {
1104
1317
  const input = normalizePath(inputRaw);
1105
- if (!existsSync3(input)) {
1318
+ if (!existsSync5(input)) {
1106
1319
  error(`File not found: ${input}`);
1107
1320
  return false;
1108
1321
  }
@@ -1170,8 +1383,8 @@ async function runGUI(inputRaw) {
1170
1383
  async function generatePreview(input, options) {
1171
1384
  const tempDir = tmpdir();
1172
1385
  const timestamp = Date.now();
1173
- const tempSource = join3(tempDir, `piclet-preview-${timestamp}-src.png`);
1174
- const tempOutput = join3(tempDir, `piclet-preview-${timestamp}.png`);
1386
+ const tempSource = join4(tempDir, `piclet-preview-${timestamp}-src.png`);
1387
+ const tempOutput = join4(tempDir, `piclet-preview-${timestamp}.png`);
1175
1388
  try {
1176
1389
  let previewInput = input;
1177
1390
  if (isMultiFrame(input)) {
@@ -1185,7 +1398,7 @@ async function generatePreview(input, options) {
1185
1398
  cleanup(tempSource, tempOutput);
1186
1399
  return { success: false, error: "Border failed" };
1187
1400
  }
1188
- const buffer = readFileSync2(tempOutput);
1401
+ const buffer = readFileSync3(tempOutput);
1189
1402
  const base64 = buffer.toString("base64");
1190
1403
  const imageData = `data:image/png;base64,${base64}`;
1191
1404
  const dims = await getDimensions(tempOutput);
@@ -1208,16 +1421,10 @@ var config = {
1208
1421
  extensions: [".png", ".jpg", ".jpeg", ".gif", ".bmp"]
1209
1422
  };
1210
1423
 
1211
- // src/cli/utils.ts
1212
- import { extname as extname3 } from "path";
1213
- import { dirname as dirname9 } from "path";
1214
- import { fileURLToPath as fileURLToPath2 } from "url";
1215
- import chalk from "chalk";
1216
-
1217
1424
  // src/tools/extract-frames.ts
1218
- import { existsSync as existsSync4, mkdirSync as mkdirSync3, readFileSync as readFileSync3 } from "fs";
1425
+ import { existsSync as existsSync6, mkdirSync as mkdirSync4, readFileSync as readFileSync4 } from "fs";
1219
1426
  import { tmpdir as tmpdir2 } from "os";
1220
- import { basename as basename3, join as join4 } from "path";
1427
+ import { basename as basename3, join as join5 } from "path";
1221
1428
  async function run2(inputRaw) {
1222
1429
  if (!await checkImageMagick()) {
1223
1430
  error("ImageMagick not found. Please install it:");
@@ -1226,7 +1433,7 @@ async function run2(inputRaw) {
1226
1433
  return false;
1227
1434
  }
1228
1435
  const input = normalizePath(inputRaw);
1229
- if (!existsSync4(input)) {
1436
+ if (!existsSync6(input)) {
1230
1437
  error(`File not found: ${input}`);
1231
1438
  await pauseOnError();
1232
1439
  return false;
@@ -1257,7 +1464,7 @@ async function run2(inputRaw) {
1257
1464
  const outputDir = `${fileInfo.dirname}/${fileInfo.filename}_frames`;
1258
1465
  console.log("");
1259
1466
  wip("Extracting frames...");
1260
- mkdirSync3(outputDir, { recursive: true });
1467
+ mkdirSync4(outputDir, { recursive: true });
1261
1468
  const frames = await extractAllFrames(input, outputDir, "frame");
1262
1469
  if (frames.length === 0) {
1263
1470
  wipDone(false, "Extraction failed");
@@ -1270,7 +1477,7 @@ async function run2(inputRaw) {
1270
1477
  }
1271
1478
  async function runGUI2(inputRaw) {
1272
1479
  const input = normalizePath(inputRaw);
1273
- if (!existsSync4(input)) {
1480
+ if (!existsSync6(input)) {
1274
1481
  error(`File not found: ${input}`);
1275
1482
  return false;
1276
1483
  }
@@ -1309,7 +1516,7 @@ async function runGUI2(inputRaw) {
1309
1516
  }
1310
1517
  logs.push({ type: "info", message: `Extracting ${frameCount} frames...` });
1311
1518
  const outputDir = `${fileInfo.dirname}/${fileInfo.filename}_frames`;
1312
- mkdirSync3(outputDir, { recursive: true });
1519
+ mkdirSync4(outputDir, { recursive: true });
1313
1520
  const frames = await extractAllFrames(input, outputDir, "frame");
1314
1521
  if (frames.length > 0) {
1315
1522
  logs.push({ type: "success", message: `Extracted ${frames.length} frames` });
@@ -1327,12 +1534,12 @@ async function runGUI2(inputRaw) {
1327
1534
  async function generateFramePreview(input, frameIndex) {
1328
1535
  const tempDir = tmpdir2();
1329
1536
  const timestamp = Date.now();
1330
- const tempOutput = join4(tempDir, `piclet-frame-${timestamp}.png`);
1537
+ const tempOutput = join5(tempDir, `piclet-frame-${timestamp}.png`);
1331
1538
  try {
1332
1539
  if (!await extractFirstFrame(input, tempOutput, frameIndex)) {
1333
1540
  return { success: false, error: "Failed to extract frame" };
1334
1541
  }
1335
- const buffer = readFileSync3(tempOutput);
1542
+ const buffer = readFileSync4(tempOutput);
1336
1543
  const base64 = buffer.toString("base64");
1337
1544
  const imageData = `data:image/png;base64,${base64}`;
1338
1545
  const dims = await getDimensions(tempOutput);
@@ -1356,9 +1563,9 @@ var config2 = {
1356
1563
  };
1357
1564
 
1358
1565
  // src/tools/filter.ts
1359
- import { existsSync as existsSync5, readFileSync as readFileSync4 } from "fs";
1566
+ import { existsSync as existsSync7, readFileSync as readFileSync5 } from "fs";
1360
1567
  import { tmpdir as tmpdir3 } from "os";
1361
- import { basename as basename4, join as join5 } from "path";
1568
+ import { basename as basename4, join as join6 } from "path";
1362
1569
  var FILTER_LABELS = {
1363
1570
  "grayscale": "Grayscale",
1364
1571
  "sepia": "Sepia",
@@ -1432,7 +1639,7 @@ async function run3(inputRaw) {
1432
1639
  return false;
1433
1640
  }
1434
1641
  const input = normalizePath(inputRaw);
1435
- if (!existsSync5(input)) {
1642
+ if (!existsSync7(input)) {
1436
1643
  error(`File not found: ${input}`);
1437
1644
  await pauseOnError();
1438
1645
  return false;
@@ -1451,7 +1658,7 @@ async function run3(inputRaw) {
1451
1658
  }
1452
1659
  async function runGUI3(inputRaw) {
1453
1660
  const input = normalizePath(inputRaw);
1454
- if (!existsSync5(input)) {
1661
+ if (!existsSync7(input)) {
1455
1662
  error(`File not found: ${input}`);
1456
1663
  return false;
1457
1664
  }
@@ -1516,8 +1723,8 @@ async function runGUI3(inputRaw) {
1516
1723
  async function generatePreview2(input, options) {
1517
1724
  const tempDir = tmpdir3();
1518
1725
  const timestamp = Date.now();
1519
- const tempSource = join5(tempDir, `piclet-preview-${timestamp}-src.png`);
1520
- const tempOutput = join5(tempDir, `piclet-preview-${timestamp}.png`);
1726
+ const tempSource = join6(tempDir, `piclet-preview-${timestamp}-src.png`);
1727
+ const tempOutput = join6(tempDir, `piclet-preview-${timestamp}.png`);
1521
1728
  try {
1522
1729
  let previewInput = input;
1523
1730
  if (isMultiFrame(input)) {
@@ -1531,7 +1738,7 @@ async function generatePreview2(input, options) {
1531
1738
  cleanup(tempSource, tempOutput);
1532
1739
  return { success: false, error: "Filter failed" };
1533
1740
  }
1534
- const buffer = readFileSync4(tempOutput);
1741
+ const buffer = readFileSync5(tempOutput);
1535
1742
  const base64 = buffer.toString("base64");
1536
1743
  const imageData = `data:image/png;base64,${base64}`;
1537
1744
  const dims = await getDimensions(tempOutput);
@@ -1555,7 +1762,7 @@ var config3 = {
1555
1762
  };
1556
1763
 
1557
1764
  // src/tools/iconpack.ts
1558
- import { existsSync as existsSync6, mkdirSync as mkdirSync4 } from "fs";
1765
+ import { existsSync as existsSync8, mkdirSync as mkdirSync5 } from "fs";
1559
1766
  import { basename as basename5, dirname as dirname5 } from "path";
1560
1767
  var WEB_ICONS = [
1561
1768
  { filename: "favicon-16x16.png", size: 16 },
@@ -1606,8 +1813,8 @@ async function generateIcons(outputDir, sourceImg, icons) {
1606
1813
  current++;
1607
1814
  const outputPath = `${outputDir}/${icon.filename}`;
1608
1815
  const subdir = dirname5(outputPath);
1609
- if (!existsSync6(subdir)) {
1610
- mkdirSync4(subdir, { recursive: true });
1816
+ if (!existsSync8(subdir)) {
1817
+ mkdirSync5(subdir, { recursive: true });
1611
1818
  }
1612
1819
  clearLine();
1613
1820
  wip(`[${current}/${total}] Generating ${icon.filename}...`);
@@ -1652,7 +1859,7 @@ async function run4(inputRaw) {
1652
1859
  return false;
1653
1860
  }
1654
1861
  const input = normalizePath(inputRaw);
1655
- if (!existsSync6(input)) {
1862
+ if (!existsSync8(input)) {
1656
1863
  error(`File not found: ${input}`);
1657
1864
  await pauseOnError();
1658
1865
  return false;
@@ -1694,7 +1901,7 @@ async function run4(inputRaw) {
1694
1901
  const doAndroid = platforms.includes("android");
1695
1902
  const doIos = platforms.includes("ios");
1696
1903
  const outputDir = `${fileInfo.dirname}/${fileInfo.filename}_icons`;
1697
- mkdirSync4(outputDir, { recursive: true });
1904
+ mkdirSync5(outputDir, { recursive: true });
1698
1905
  info(`Output directory: ${outputDir}`);
1699
1906
  console.log("");
1700
1907
  wip("Preparing source image...");
@@ -1716,7 +1923,7 @@ async function run4(inputRaw) {
1716
1923
  console.log("");
1717
1924
  header("Web Icons");
1718
1925
  const webDir = `${outputDir}/web`;
1719
- mkdirSync4(webDir, { recursive: true });
1926
+ mkdirSync5(webDir, { recursive: true });
1720
1927
  if (!await generateFavicon(webDir, tempSource)) {
1721
1928
  totalFailed++;
1722
1929
  }
@@ -1726,14 +1933,14 @@ async function run4(inputRaw) {
1726
1933
  console.log("");
1727
1934
  header("Android Icons");
1728
1935
  const androidDir = `${outputDir}/android`;
1729
- mkdirSync4(androidDir, { recursive: true });
1936
+ mkdirSync5(androidDir, { recursive: true });
1730
1937
  totalFailed += await generateIcons(androidDir, tempSource, ANDROID_ICONS);
1731
1938
  }
1732
1939
  if (doIos) {
1733
1940
  console.log("");
1734
1941
  header("iOS Icons");
1735
1942
  const iosDir = `${outputDir}/ios`;
1736
- mkdirSync4(iosDir, { recursive: true });
1943
+ mkdirSync5(iosDir, { recursive: true });
1737
1944
  totalFailed += await generateIcons(iosDir, tempSource, IOS_ICONS);
1738
1945
  }
1739
1946
  cleanup(tempSource);
@@ -1759,7 +1966,7 @@ async function run4(inputRaw) {
1759
1966
  }
1760
1967
  async function runGUI4(inputRaw) {
1761
1968
  const input = normalizePath(inputRaw);
1762
- if (!existsSync6(input)) {
1969
+ if (!existsSync8(input)) {
1763
1970
  error(`File not found: ${input}`);
1764
1971
  return false;
1765
1972
  }
@@ -1804,7 +2011,7 @@ async function runGUI4(inputRaw) {
1804
2011
  };
1805
2012
  }
1806
2013
  const outputDir = `${fileInfo.dirname}/${fileInfo.filename}_icons`;
1807
- mkdirSync4(outputDir, { recursive: true });
2014
+ mkdirSync5(outputDir, { recursive: true });
1808
2015
  logs.push({ type: "info", message: `Output: ${outputDir}` });
1809
2016
  logs.push({ type: "info", message: "Preparing source image..." });
1810
2017
  const tempSource = `${outputDir}/.source_1024.png`;
@@ -1822,7 +2029,7 @@ async function runGUI4(inputRaw) {
1822
2029
  if (doWeb) {
1823
2030
  logs.push({ type: "info", message: "Generating Web icons..." });
1824
2031
  const webDir = `${outputDir}/web`;
1825
- mkdirSync4(webDir, { recursive: true });
2032
+ mkdirSync5(webDir, { recursive: true });
1826
2033
  if (!await generateFaviconSilent(webDir, tempSource)) {
1827
2034
  totalFailed++;
1828
2035
  }
@@ -1832,14 +2039,14 @@ async function runGUI4(inputRaw) {
1832
2039
  if (doAndroid) {
1833
2040
  logs.push({ type: "info", message: "Generating Android icons..." });
1834
2041
  const androidDir = `${outputDir}/android`;
1835
- mkdirSync4(androidDir, { recursive: true });
2042
+ mkdirSync5(androidDir, { recursive: true });
1836
2043
  totalFailed += await generateIconsSilent(androidDir, tempSource, ANDROID_ICONS, logs);
1837
2044
  logs.push({ type: "success", message: `Android: ${ANDROID_ICONS.length} icons` });
1838
2045
  }
1839
2046
  if (doIos) {
1840
2047
  logs.push({ type: "info", message: "Generating iOS icons..." });
1841
2048
  const iosDir = `${outputDir}/ios`;
1842
- mkdirSync4(iosDir, { recursive: true });
2049
+ mkdirSync5(iosDir, { recursive: true });
1843
2050
  totalFailed += await generateIconsSilent(iosDir, tempSource, IOS_ICONS, logs);
1844
2051
  logs.push({ type: "success", message: `iOS: ${IOS_ICONS.length} icons` });
1845
2052
  }
@@ -1864,8 +2071,8 @@ async function generateIconsSilent(outputDir, sourceImg, icons, _logs) {
1864
2071
  for (const icon of icons) {
1865
2072
  const outputPath = `${outputDir}/${icon.filename}`;
1866
2073
  const subdir = dirname5(outputPath);
1867
- if (!existsSync6(subdir)) {
1868
- mkdirSync4(subdir, { recursive: true });
2074
+ if (!existsSync8(subdir)) {
2075
+ mkdirSync5(subdir, { recursive: true });
1869
2076
  }
1870
2077
  if (!await scaleToSize(sourceImg, outputPath, icon.size)) {
1871
2078
  failed++;
@@ -1895,9 +2102,9 @@ var config4 = {
1895
2102
  };
1896
2103
 
1897
2104
  // src/tools/makeicon.ts
1898
- import { existsSync as existsSync7, readFileSync as readFileSync5 } from "fs";
2105
+ import { existsSync as existsSync9, readFileSync as readFileSync6 } from "fs";
1899
2106
  import { tmpdir as tmpdir4 } from "os";
1900
- import { basename as basename6, join as join6 } from "path";
2107
+ import { basename as basename6, join as join7 } from "path";
1901
2108
  async function run5(inputRaw) {
1902
2109
  if (!await checkImageMagick()) {
1903
2110
  error("ImageMagick not found. Please install it:");
@@ -1906,7 +2113,7 @@ async function run5(inputRaw) {
1906
2113
  return false;
1907
2114
  }
1908
2115
  const input = normalizePath(inputRaw);
1909
- if (!existsSync7(input)) {
2116
+ if (!existsSync9(input)) {
1910
2117
  error(`File not found: ${input}`);
1911
2118
  await pauseOnError();
1912
2119
  return false;
@@ -2004,9 +2211,9 @@ async function processForIcon(input, output, options, logs) {
2004
2211
  async function generatePreview3(input, options) {
2005
2212
  const tempDir = tmpdir4();
2006
2213
  const timestamp = Date.now();
2007
- const tempTrimmed = join6(tempDir, `piclet-preview-trimmed-${timestamp}.png`);
2008
- const tempSquare = join6(tempDir, `piclet-preview-square-${timestamp}.png`);
2009
- const tempOutput = join6(tempDir, `piclet-preview-${timestamp}.png`);
2214
+ const tempTrimmed = join7(tempDir, `piclet-preview-trimmed-${timestamp}.png`);
2215
+ const tempSquare = join7(tempDir, `piclet-preview-square-${timestamp}.png`);
2216
+ const tempOutput = join7(tempDir, `piclet-preview-${timestamp}.png`);
2010
2217
  try {
2011
2218
  let currentInput = input;
2012
2219
  if (options.trim) {
@@ -2030,7 +2237,7 @@ async function generatePreview3(input, options) {
2030
2237
  }
2031
2238
  if (currentInput === tempSquare) cleanup(tempSquare);
2032
2239
  else if (currentInput === tempTrimmed) cleanup(tempTrimmed);
2033
- const buffer = readFileSync5(tempOutput);
2240
+ const buffer = readFileSync6(tempOutput);
2034
2241
  const base64 = buffer.toString("base64");
2035
2242
  const imageData = `data:image/png;base64,${base64}`;
2036
2243
  const dims = await getDimensions(tempOutput);
@@ -2048,7 +2255,7 @@ async function generatePreview3(input, options) {
2048
2255
  }
2049
2256
  async function runGUI5(inputRaw) {
2050
2257
  const input = normalizePath(inputRaw);
2051
- if (!existsSync7(input)) {
2258
+ if (!existsSync9(input)) {
2052
2259
  error(`File not found: ${input}`);
2053
2260
  return false;
2054
2261
  }
@@ -2113,16 +2320,16 @@ var config5 = {
2113
2320
  };
2114
2321
 
2115
2322
  // src/tools/piclet-main.ts
2116
- import { existsSync as existsSync8, mkdirSync as mkdirSync5, readFileSync as readFileSync6, renameSync, writeFileSync as writeFileSync2 } from "fs";
2323
+ import { existsSync as existsSync10, mkdirSync as mkdirSync6, readFileSync as readFileSync7, renameSync, writeFileSync as writeFileSync2 } from "fs";
2117
2324
  import { tmpdir as tmpdir5 } from "os";
2118
- import { basename as basename7, dirname as dirname6, extname as extname2, join as join7 } from "path";
2325
+ import { basename as basename7, dirname as dirname6, extname as extname3, join as join8 } from "path";
2119
2326
  var TOOL_ORDER = ["removebg", "scale", "icons", "storepack"];
2120
2327
  async function generateCombinedPreview(input, borderColor, opts) {
2121
2328
  const tempDir = tmpdir5();
2122
2329
  const ts = Date.now();
2123
2330
  const temps = [];
2124
2331
  const makeTempPath = (suffix) => {
2125
- const p = join7(tempDir, `piclet-${ts}-${suffix}.png`);
2332
+ const p = join8(tempDir, `piclet-${ts}-${suffix}.png`);
2126
2333
  temps.push(p);
2127
2334
  return p;
2128
2335
  };
@@ -2144,7 +2351,7 @@ async function generateCombinedPreview(input, borderColor, opts) {
2144
2351
  previewPath2 = scaled;
2145
2352
  }
2146
2353
  }
2147
- const buffer2 = readFileSync6(previewPath2);
2354
+ const buffer2 = readFileSync7(previewPath2);
2148
2355
  const finalDims2 = await getDimensions(previewPath2);
2149
2356
  cleanup(...temps);
2150
2357
  return {
@@ -2161,7 +2368,9 @@ async function generateCombinedPreview(input, borderColor, opts) {
2161
2368
  const rbOpts = opts.removebg;
2162
2369
  const out = makeTempPath("removebg");
2163
2370
  let success2 = false;
2164
- if (rbOpts.preserveInner && borderColor) {
2371
+ if (rbOpts.edgeDetect && borderColor) {
2372
+ success2 = await removeBackgroundEdgeAware(current, out, borderColor, rbOpts.fuzz, rbOpts.edgeStrength);
2373
+ } else if (rbOpts.preserveInner && borderColor) {
2165
2374
  success2 = await removeBackgroundBorderOnly(current, out, borderColor, rbOpts.fuzz);
2166
2375
  }
2167
2376
  if (!success2 && borderColor) {
@@ -2227,7 +2436,7 @@ async function generateCombinedPreview(input, borderColor, opts) {
2227
2436
  previewPath = scaled;
2228
2437
  }
2229
2438
  }
2230
- const buffer = readFileSync6(previewPath);
2439
+ const buffer = readFileSync7(previewPath);
2231
2440
  const finalDims = await getDimensions(previewPath);
2232
2441
  cleanup(...temps);
2233
2442
  return {
@@ -2245,6 +2454,7 @@ async function processCombined(input, borderColor, opts, logs) {
2245
2454
  const fileInfo = getFileInfo(input);
2246
2455
  const outputs = [];
2247
2456
  const temps = [];
2457
+ let singleFilePath;
2248
2458
  const outputExt = fileInfo.extension.toLowerCase() === ".gif" ? ".gif" : ".png";
2249
2459
  const makeTempPath = (suffix) => {
2250
2460
  const p = `${fileInfo.dirname}/${fileInfo.filename}_${suffix}${outputExt}`;
@@ -2260,7 +2470,13 @@ async function processCombined(input, borderColor, opts, logs) {
2260
2470
  const rbOpts = opts.removebg;
2261
2471
  const out = makeTempPath("nobg");
2262
2472
  let success2 = false;
2263
- if (rbOpts.preserveInner && borderColor) {
2473
+ if (rbOpts.edgeDetect && borderColor) {
2474
+ logs.push({ type: "info", message: "Using edge feathering..." });
2475
+ success2 = await removeBackgroundEdgeAware(current, out, borderColor, rbOpts.fuzz, rbOpts.edgeStrength);
2476
+ if (!success2) {
2477
+ logs.push({ type: "warn", message: "Edge feathering failed, trying standard removal" });
2478
+ }
2479
+ } else if (rbOpts.preserveInner && borderColor) {
2264
2480
  success2 = await removeBackgroundBorderOnly(current, out, borderColor, rbOpts.fuzz);
2265
2481
  if (!success2) {
2266
2482
  logs.push({ type: "warn", message: "Border-only failed, trying full removal" });
@@ -2272,7 +2488,7 @@ async function processCombined(input, borderColor, opts, logs) {
2272
2488
  if (!success2) {
2273
2489
  logs.push({ type: "error", message: "Background removal failed" });
2274
2490
  cleanup(...temps);
2275
- return [];
2491
+ return { outputs: [] };
2276
2492
  }
2277
2493
  logs.push({ type: "success", message: "Background removed" });
2278
2494
  if (rbOpts.trim) {
@@ -2290,10 +2506,12 @@ async function processCombined(input, borderColor, opts, logs) {
2290
2506
  current = out;
2291
2507
  }
2292
2508
  if (activeTools.indexOf(tool) === activeTools.length - 1) {
2293
- const finalOut = `${fileInfo.dirname}/${fileInfo.filename}_nobg${outputExt}`;
2509
+ const outDir = ensureOutputDir(input);
2510
+ const finalOut = join8(outDir, `${fileInfo.filename}_nobg${outputExt}`);
2294
2511
  renameSync(current, finalOut);
2295
2512
  temps.splice(temps.indexOf(current), 1);
2296
2513
  outputs.push(basename7(finalOut));
2514
+ singleFilePath = finalOut;
2297
2515
  }
2298
2516
  break;
2299
2517
  }
@@ -2311,7 +2529,7 @@ async function processCombined(input, borderColor, opts, logs) {
2311
2529
  if (!success2) {
2312
2530
  logs.push({ type: "error", message: "Scale failed" });
2313
2531
  cleanup(...temps);
2314
- return [];
2532
+ return { outputs: [] };
2315
2533
  }
2316
2534
  const dims = await getDimensions(out);
2317
2535
  logs.push({ type: "success", message: `Scaled to ${dims?.[0]}\xD7${dims?.[1]}` });
@@ -2321,10 +2539,12 @@ async function processCombined(input, borderColor, opts, logs) {
2321
2539
  }
2322
2540
  current = out;
2323
2541
  if (activeTools.indexOf(tool) === activeTools.length - 1) {
2324
- const finalOut = `${fileInfo.dirname}/${fileInfo.filename}_scaled${outputExt}`;
2542
+ const outDir = ensureOutputDir(input);
2543
+ const finalOut = join8(outDir, `${fileInfo.filename}_scaled${outputExt}`);
2325
2544
  renameSync(current, finalOut);
2326
2545
  temps.splice(temps.indexOf(current), 1);
2327
2546
  outputs.push(basename7(finalOut));
2547
+ singleFilePath = finalOut;
2328
2548
  }
2329
2549
  break;
2330
2550
  }
@@ -2333,7 +2553,7 @@ async function processCombined(input, borderColor, opts, logs) {
2333
2553
  const icOpts = opts.icons;
2334
2554
  if (!icOpts.ico && !icOpts.web && !icOpts.android && !icOpts.ios) {
2335
2555
  logs.push({ type: "error", message: "No output format selected" });
2336
- return [];
2556
+ return { outputs: [] };
2337
2557
  }
2338
2558
  let iconSource = current;
2339
2559
  if (icOpts.trim && current === input) {
@@ -2358,13 +2578,14 @@ async function processCombined(input, borderColor, opts, logs) {
2358
2578
  if (!await scaleToSize(iconSource, srcTemp, maxSize)) {
2359
2579
  logs.push({ type: "error", message: "Failed to prepare icon source" });
2360
2580
  cleanup(...temps);
2361
- return [];
2581
+ return { outputs: [] };
2362
2582
  }
2363
2583
  if (iconSource !== current && iconSource !== input) cleanup(iconSource);
2364
2584
  let totalCount = 0;
2365
2585
  if (icOpts.ico) {
2366
2586
  logs.push({ type: "info", message: "Creating ICO file..." });
2367
- const icoOut = `${fileInfo.dirname}/${fileInfo.filename}.ico`;
2587
+ const outDir = ensureOutputDir(input);
2588
+ const icoOut = join8(outDir, `${fileInfo.filename}.ico`);
2368
2589
  if (await createIco(srcTemp, icoOut)) {
2369
2590
  logs.push({ type: "success", message: "ICO: 6 sizes (256, 128, 64, 48, 32, 16)" });
2370
2591
  outputs.push(basename7(icoOut));
@@ -2375,12 +2596,13 @@ async function processCombined(input, borderColor, opts, logs) {
2375
2596
  }
2376
2597
  const needsPacks = icOpts.web || icOpts.android || icOpts.ios;
2377
2598
  if (needsPacks) {
2378
- const outputDir = `${fileInfo.dirname}/${fileInfo.filename}_icons`;
2379
- mkdirSync5(outputDir, { recursive: true });
2599
+ const outDir = ensureOutputDir(input);
2600
+ const outputDir = join8(outDir, `${fileInfo.filename}_icons`);
2601
+ mkdirSync6(outputDir, { recursive: true });
2380
2602
  if (icOpts.web) {
2381
2603
  logs.push({ type: "info", message: "Generating Web icons..." });
2382
2604
  const webDir = `${outputDir}/web`;
2383
- mkdirSync5(webDir, { recursive: true });
2605
+ mkdirSync6(webDir, { recursive: true });
2384
2606
  const t16 = `${webDir}/.t16.png`, t32 = `${webDir}/.t32.png`, t48 = `${webDir}/.t48.png`;
2385
2607
  await scaleToSize(srcTemp, t16, 16);
2386
2608
  await scaleToSize(srcTemp, t32, 32);
@@ -2415,7 +2637,7 @@ async function processCombined(input, borderColor, opts, logs) {
2415
2637
  ];
2416
2638
  for (const i of androidIcons) {
2417
2639
  const p = `${androidDir}/${i.name}`;
2418
- mkdirSync5(dirname6(p), { recursive: true });
2640
+ mkdirSync6(dirname6(p), { recursive: true });
2419
2641
  await scaleToSize(srcTemp, p, i.size);
2420
2642
  totalCount++;
2421
2643
  }
@@ -2424,7 +2646,7 @@ async function processCombined(input, borderColor, opts, logs) {
2424
2646
  if (icOpts.ios) {
2425
2647
  logs.push({ type: "info", message: "Generating iOS icons..." });
2426
2648
  const iosDir = `${outputDir}/ios`;
2427
- mkdirSync5(iosDir, { recursive: true });
2649
+ mkdirSync6(iosDir, { recursive: true });
2428
2650
  const iosSizes = [20, 29, 40, 58, 60, 76, 80, 87, 120, 152, 167, 180, 1024];
2429
2651
  for (const s of iosSizes) {
2430
2652
  await scaleToSize(srcTemp, `${iosDir}/AppIcon-${s}.png`, s);
@@ -2443,15 +2665,16 @@ async function processCombined(input, borderColor, opts, logs) {
2443
2665
  const spOpts = opts.storepack;
2444
2666
  if (!spOpts.dimensions || spOpts.dimensions.length === 0) {
2445
2667
  logs.push({ type: "error", message: "No dimensions specified" });
2446
- return [];
2668
+ return { outputs: [] };
2447
2669
  }
2448
2670
  const folderName = spOpts.presetName || "assets";
2449
- const outputDir = `${fileInfo.dirname}/${fileInfo.filename}_${folderName}`;
2450
- mkdirSync5(outputDir, { recursive: true });
2671
+ const outDir = ensureOutputDir(input);
2672
+ const outputDir = join8(outDir, `${fileInfo.filename}_${folderName}`);
2673
+ mkdirSync6(outputDir, { recursive: true });
2451
2674
  let count = 0;
2452
2675
  for (const dim of spOpts.dimensions) {
2453
2676
  const filename = dim.filename || `${dim.width}x${dim.height}.png`;
2454
- const out = join7(outputDir, filename);
2677
+ const out = join8(outputDir, filename);
2455
2678
  let success2 = false;
2456
2679
  switch (spOpts.scaleMode) {
2457
2680
  case "fill":
@@ -2472,11 +2695,11 @@ async function processCombined(input, borderColor, opts, logs) {
2472
2695
  }
2473
2696
  }
2474
2697
  cleanup(...temps);
2475
- return outputs;
2698
+ return { outputs, singleFilePath };
2476
2699
  }
2477
2700
  async function runGUI6(inputRaw) {
2478
2701
  let currentInput = normalizePath(inputRaw);
2479
- if (!existsSync8(currentInput)) {
2702
+ if (!existsSync10(currentInput)) {
2480
2703
  error(`File not found: ${currentInput}`);
2481
2704
  return false;
2482
2705
  }
@@ -2490,14 +2713,14 @@ async function runGUI6(inputRaw) {
2490
2713
  const presets = loadPresets();
2491
2714
  async function generateFrameThumbnail(frameIndex) {
2492
2715
  const tempDir = tmpdir5();
2493
- const tempOutput = join7(tempDir, `piclet-frame-${Date.now()}-${frameIndex}.png`);
2716
+ const tempOutput = join8(tempDir, `piclet-frame-${Date.now()}-${frameIndex}.png`);
2494
2717
  try {
2495
2718
  if (!await extractFirstFrame(currentInput, tempOutput, frameIndex)) {
2496
2719
  return { success: false, error: "Failed to extract frame" };
2497
2720
  }
2498
- const thumbOutput = join7(tempDir, `piclet-thumb-${Date.now()}-${frameIndex}.png`);
2721
+ const thumbOutput = join8(tempDir, `piclet-thumb-${Date.now()}-${frameIndex}.png`);
2499
2722
  await scaleToSize(tempOutput, thumbOutput, 96);
2500
- const buffer = readFileSync6(thumbOutput);
2723
+ const buffer = readFileSync7(thumbOutput);
2501
2724
  cleanup(tempOutput, thumbOutput);
2502
2725
  return {
2503
2726
  success: true,
@@ -2511,7 +2734,7 @@ async function runGUI6(inputRaw) {
2511
2734
  async function generateFramePreview2(frameIndex, opts) {
2512
2735
  const tempDir = tmpdir5();
2513
2736
  const ts = Date.now();
2514
- const frameFile = join7(tempDir, `piclet-fp-${ts}-${frameIndex}.png`);
2737
+ const frameFile = join8(tempDir, `piclet-fp-${ts}-${frameIndex}.png`);
2515
2738
  const temps = [frameFile];
2516
2739
  try {
2517
2740
  if (!await extractFirstFrame(currentInput, frameFile, frameIndex)) {
@@ -2520,7 +2743,7 @@ async function runGUI6(inputRaw) {
2520
2743
  let current = frameFile;
2521
2744
  const activeTools = ["removebg", "scale", "icons"].filter((t) => opts.tools.includes(t));
2522
2745
  for (const tool of activeTools) {
2523
- const tempOut = join7(tempDir, `piclet-fp-${ts}-${frameIndex}-${tool}.png`);
2746
+ const tempOut = join8(tempDir, `piclet-fp-${ts}-${frameIndex}-${tool}.png`);
2524
2747
  temps.push(tempOut);
2525
2748
  switch (tool) {
2526
2749
  case "removebg": {
@@ -2534,7 +2757,7 @@ async function runGUI6(inputRaw) {
2534
2757
  }
2535
2758
  if (success2) {
2536
2759
  if (rbOpts.trim) {
2537
- const trimOut = join7(tempDir, `piclet-fp-${ts}-${frameIndex}-trim.png`);
2760
+ const trimOut = join8(tempDir, `piclet-fp-${ts}-${frameIndex}-trim.png`);
2538
2761
  temps.push(trimOut);
2539
2762
  if (await trim(tempOut, trimOut)) {
2540
2763
  current = trimOut;
@@ -2564,7 +2787,7 @@ async function runGUI6(inputRaw) {
2564
2787
  case "icons": {
2565
2788
  const icOpts = opts.icons;
2566
2789
  if (icOpts.trim) {
2567
- const trimOut = join7(tempDir, `piclet-fp-${ts}-${frameIndex}-ictrim.png`);
2790
+ const trimOut = join8(tempDir, `piclet-fp-${ts}-${frameIndex}-ictrim.png`);
2568
2791
  temps.push(trimOut);
2569
2792
  if (await trim(current, trimOut)) {
2570
2793
  current = trimOut;
@@ -2579,10 +2802,10 @@ async function runGUI6(inputRaw) {
2579
2802
  }
2580
2803
  }
2581
2804
  }
2582
- const thumbOut = join7(tempDir, `piclet-fp-${ts}-${frameIndex}-thumb.png`);
2805
+ const thumbOut = join8(tempDir, `piclet-fp-${ts}-${frameIndex}-thumb.png`);
2583
2806
  temps.push(thumbOut);
2584
2807
  await scaleToSize(current, thumbOut, 96);
2585
- const buffer = readFileSync6(thumbOut);
2808
+ const buffer = readFileSync7(thumbOut);
2586
2809
  cleanup(...temps);
2587
2810
  return {
2588
2811
  success: true,
@@ -2621,7 +2844,7 @@ async function runGUI6(inputRaw) {
2621
2844
  if (isMultiFrame(currentInput) && typeof toolOpts.frameIndex === "number") {
2622
2845
  const tempDir = tmpdir5();
2623
2846
  const ts = Date.now();
2624
- const frameFile = join7(tempDir, `piclet-prev-${ts}.png`);
2847
+ const frameFile = join8(tempDir, `piclet-prev-${ts}.png`);
2625
2848
  if (!await extractFirstFrame(currentInput, frameFile, toolOpts.frameIndex)) {
2626
2849
  return { success: false, error: "Failed to extract frame" };
2627
2850
  }
@@ -2647,11 +2870,12 @@ async function runGUI6(inputRaw) {
2647
2870
  logs: [{ type: "error", message: "ImageMagick not found. Install: sudo apt install imagemagick" }]
2648
2871
  };
2649
2872
  }
2650
- const outputs = await processCombined(currentInput, currentBorderColor, toolOpts, logs);
2651
- if (outputs.length > 0) {
2873
+ const result = await processCombined(currentInput, currentBorderColor, toolOpts, logs);
2874
+ if (result.outputs.length > 0) {
2652
2875
  return {
2653
2876
  success: true,
2654
- output: outputs.join("\n"),
2877
+ output: result.outputs.join("\n"),
2878
+ outputPath: result.singleFilePath,
2655
2879
  logs
2656
2880
  };
2657
2881
  }
@@ -2659,8 +2883,8 @@ async function runGUI6(inputRaw) {
2659
2883
  },
2660
2884
  onLoadImage: async (data) => {
2661
2885
  try {
2662
- const ext = extname2(data.fileName) || ".png";
2663
- const tempPath = join7(tmpdir5(), `piclet-load-${Date.now()}${ext}`);
2886
+ const ext = extname3(data.fileName) || ".png";
2887
+ const tempPath = join8(tmpdir5(), `piclet-load-${Date.now()}${ext}`);
2664
2888
  const buffer = Buffer.from(data.data, "base64");
2665
2889
  writeFileSync2(tempPath, buffer);
2666
2890
  const newDims = await getDimensions(tempPath);
@@ -2691,6 +2915,64 @@ async function runGUI6(inputRaw) {
2691
2915
  const toolOpts = opts;
2692
2916
  if (!toolOpts.tools) toolOpts.tools = [];
2693
2917
  return generateFramePreview2(frameIndex, toolOpts);
2918
+ },
2919
+ onSimplifyGif: async (skipFactor) => {
2920
+ try {
2921
+ const tempPath = join8(tmpdir5(), `piclet-simplified-${Date.now()}.gif`);
2922
+ const result = await simplifyGif(currentInput, tempPath, skipFactor);
2923
+ if (!result.success) {
2924
+ return { success: false, error: "Failed to simplify GIF" };
2925
+ }
2926
+ const newDims = await getDimensions(tempPath);
2927
+ if (!newDims) {
2928
+ cleanup(tempPath);
2929
+ return { success: false, error: "Failed to read simplified GIF dimensions" };
2930
+ }
2931
+ currentInput = tempPath;
2932
+ currentFrameCount = result.frameCount ?? 1;
2933
+ return {
2934
+ success: true,
2935
+ filePath: tempPath,
2936
+ fileName: basename7(currentInput),
2937
+ width: newDims[0],
2938
+ height: newDims[1],
2939
+ frameCount: currentFrameCount
2940
+ };
2941
+ } catch (err) {
2942
+ return { success: false, error: err.message };
2943
+ }
2944
+ },
2945
+ onDeleteFrame: async (frameIndex) => {
2946
+ try {
2947
+ const tempPath = join8(tmpdir5(), `piclet-edited-${Date.now()}.gif`);
2948
+ const result = await deleteGifFrame(currentInput, tempPath, frameIndex);
2949
+ if (!result.success) {
2950
+ return { success: false, error: "Failed to delete frame" };
2951
+ }
2952
+ currentInput = tempPath;
2953
+ currentFrameCount = result.frameCount ?? 1;
2954
+ return { success: true, frameCount: currentFrameCount };
2955
+ } catch (err) {
2956
+ return { success: false, error: err.message };
2957
+ }
2958
+ },
2959
+ onReplaceFrame: async (frameIndex, imageData) => {
2960
+ try {
2961
+ const buffer = Buffer.from(imageData, "base64");
2962
+ const tempImagePath = join8(tmpdir5(), `piclet-replace-${Date.now()}.png`);
2963
+ writeFileSync2(tempImagePath, buffer);
2964
+ const tempPath = join8(tmpdir5(), `piclet-edited-${Date.now()}.gif`);
2965
+ const result = await replaceGifFrame(currentInput, tempPath, frameIndex, tempImagePath);
2966
+ cleanup(tempImagePath);
2967
+ if (!result.success) {
2968
+ return { success: false, error: "Failed to replace frame" };
2969
+ }
2970
+ currentInput = tempPath;
2971
+ currentFrameCount = result.frameCount ?? currentFrameCount;
2972
+ return { success: true, frameCount: currentFrameCount };
2973
+ } catch (err) {
2974
+ return { success: false, error: err.message };
2975
+ }
2694
2976
  }
2695
2977
  });
2696
2978
  }
@@ -2709,7 +2991,7 @@ async function processGifExport(input, borderColor, opts, logs) {
2709
2991
  case "frame": {
2710
2992
  const frameIndex = opts.frameIndex ?? 0;
2711
2993
  logs.push({ type: "info", message: `Exporting frame ${frameIndex + 1}...` });
2712
- const frameFile = join7(tempDir, `piclet-export-${ts}.png`);
2994
+ const frameFile = join8(tempDir, `piclet-export-${ts}.png`);
2713
2995
  if (!await extractFirstFrame(input, frameFile, frameIndex)) {
2714
2996
  logs.push({ type: "error", message: "Failed to extract frame" });
2715
2997
  return { success: false, error: "Failed to extract frame", logs };
@@ -2717,14 +2999,15 @@ async function processGifExport(input, borderColor, opts, logs) {
2717
2999
  let outputFile = frameFile;
2718
3000
  if (opts.tools && opts.tools.length > 0) {
2719
3001
  const processedLogs = [];
2720
- const outputs = await processCombined(frameFile, borderColor, opts, processedLogs);
3002
+ const result = await processCombined(frameFile, borderColor, opts, processedLogs);
2721
3003
  logs.push(...processedLogs);
2722
- if (outputs.length === 0) {
3004
+ if (result.outputs.length === 0) {
2723
3005
  cleanup(frameFile);
2724
3006
  return { success: false, error: "Processing failed", logs };
2725
3007
  }
2726
3008
  }
2727
- const finalOutput = `${fileInfo.dirname}/${fileInfo.filename}_frame${frameIndex + 1}.png`;
3009
+ const frameOutDir = ensureOutputDir(input);
3010
+ const finalOutput = join8(frameOutDir, `${fileInfo.filename}_frame${frameIndex + 1}.png`);
2728
3011
  if (outputFile === frameFile) {
2729
3012
  renameSync(frameFile, finalOutput);
2730
3013
  }
@@ -2733,8 +3016,9 @@ async function processGifExport(input, borderColor, opts, logs) {
2733
3016
  }
2734
3017
  case "all-frames": {
2735
3018
  logs.push({ type: "info", message: "Extracting all frames..." });
2736
- const outputDir = `${fileInfo.dirname}/${fileInfo.filename}_frames`;
2737
- mkdirSync5(outputDir, { recursive: true });
3019
+ const framesOutDir = ensureOutputDir(input);
3020
+ const outputDir = join8(framesOutDir, `${fileInfo.filename}_frames`);
3021
+ mkdirSync6(outputDir, { recursive: true });
2738
3022
  const frames = await extractAllFrames(input, outputDir, "frame");
2739
3023
  if (frames.length === 0) {
2740
3024
  logs.push({ type: "error", message: "Failed to extract frames" });
@@ -2753,9 +3037,9 @@ async function processGifExport(input, borderColor, opts, logs) {
2753
3037
  }
2754
3038
  case "gif": {
2755
3039
  logs.push({ type: "info", message: "Processing GIF..." });
2756
- const outputs = await processCombined(input, borderColor, opts, logs);
2757
- if (outputs.length > 0) {
2758
- return { success: true, output: outputs.join("\n"), logs };
3040
+ const result = await processCombined(input, borderColor, opts, logs);
3041
+ if (result.outputs.length > 0) {
3042
+ return { success: true, output: result.outputs.join("\n"), outputPath: result.singleFilePath, logs };
2759
3043
  }
2760
3044
  return { success: false, error: "Processing failed", logs };
2761
3045
  }
@@ -2771,9 +3055,9 @@ var config6 = {
2771
3055
  };
2772
3056
 
2773
3057
  // src/tools/recolor.ts
2774
- import { existsSync as existsSync9, readFileSync as readFileSync7 } from "fs";
3058
+ import { existsSync as existsSync11, readFileSync as readFileSync8 } from "fs";
2775
3059
  import { tmpdir as tmpdir6 } from "os";
2776
- import { basename as basename8, join as join8 } from "path";
3060
+ import { basename as basename8, join as join9 } from "path";
2777
3061
  async function processImage3(input, options) {
2778
3062
  const fileInfo = getFileInfo(input);
2779
3063
  const outputExt = fileInfo.extension.toLowerCase() === ".gif" ? ".gif" : ".png";
@@ -2836,7 +3120,7 @@ async function run6(inputRaw) {
2836
3120
  return false;
2837
3121
  }
2838
3122
  const input = normalizePath(inputRaw);
2839
- if (!existsSync9(input)) {
3123
+ if (!existsSync11(input)) {
2840
3124
  error(`File not found: ${input}`);
2841
3125
  await pauseOnError();
2842
3126
  return false;
@@ -2859,7 +3143,7 @@ async function run6(inputRaw) {
2859
3143
  }
2860
3144
  async function runGUI7(inputRaw) {
2861
3145
  const input = normalizePath(inputRaw);
2862
- if (!existsSync9(input)) {
3146
+ if (!existsSync11(input)) {
2863
3147
  error(`File not found: ${input}`);
2864
3148
  return false;
2865
3149
  }
@@ -2931,8 +3215,8 @@ async function runGUI7(inputRaw) {
2931
3215
  async function generatePreview4(input, options) {
2932
3216
  const tempDir = tmpdir6();
2933
3217
  const timestamp = Date.now();
2934
- const tempSource = join8(tempDir, `piclet-preview-${timestamp}-src.png`);
2935
- const tempOutput = join8(tempDir, `piclet-preview-${timestamp}.png`);
3218
+ const tempSource = join9(tempDir, `piclet-preview-${timestamp}-src.png`);
3219
+ const tempOutput = join9(tempDir, `piclet-preview-${timestamp}.png`);
2936
3220
  try {
2937
3221
  let previewInput = input;
2938
3222
  if (isMultiFrame(input)) {
@@ -2952,7 +3236,7 @@ async function generatePreview4(input, options) {
2952
3236
  cleanup(tempSource, tempOutput);
2953
3237
  return { success: false, error: "Color replacement failed" };
2954
3238
  }
2955
- const buffer = readFileSync7(tempOutput);
3239
+ const buffer = readFileSync8(tempOutput);
2956
3240
  const base64 = buffer.toString("base64");
2957
3241
  const imageData = `data:image/png;base64,${base64}`;
2958
3242
  const dims = await getDimensions(tempOutput);
@@ -2976,13 +3260,13 @@ var config7 = {
2976
3260
  };
2977
3261
 
2978
3262
  // src/tools/remove-bg.ts
2979
- import { existsSync as existsSync11, readFileSync as readFileSync9, renameSync as renameSync2 } from "fs";
3263
+ import { existsSync as existsSync13, readFileSync as readFileSync10, renameSync as renameSync2 } from "fs";
2980
3264
  import { tmpdir as tmpdir7 } from "os";
2981
- import { basename as basename9, join as join10 } from "path";
3265
+ import { basename as basename9, join as join11 } from "path";
2982
3266
 
2983
3267
  // src/lib/config.ts
2984
- import { existsSync as existsSync10, mkdirSync as mkdirSync6, readFileSync as readFileSync8, writeFileSync as writeFileSync3 } from "fs";
2985
- import { dirname as dirname7, join as join9 } from "path";
3268
+ import { existsSync as existsSync12, mkdirSync as mkdirSync7, readFileSync as readFileSync9, writeFileSync as writeFileSync3 } from "fs";
3269
+ import { dirname as dirname7, join as join10 } from "path";
2986
3270
  import { homedir as homedir2 } from "os";
2987
3271
  var DEFAULT_CONFIG = {
2988
3272
  removeBg: {
@@ -3000,18 +3284,18 @@ var DEFAULT_CONFIG = {
3000
3284
  }
3001
3285
  };
3002
3286
  function getConfigDir() {
3003
- return join9(homedir2(), ".config", "piclet");
3287
+ return join10(homedir2(), ".config", "piclet");
3004
3288
  }
3005
3289
  function getConfigPath() {
3006
- return join9(getConfigDir(), "config.json");
3290
+ return join10(getConfigDir(), "config.json");
3007
3291
  }
3008
3292
  function loadConfig() {
3009
3293
  const configPath = getConfigPath();
3010
- if (!existsSync10(configPath)) {
3294
+ if (!existsSync12(configPath)) {
3011
3295
  return { ...DEFAULT_CONFIG };
3012
3296
  }
3013
3297
  try {
3014
- const content = readFileSync8(configPath, "utf-8");
3298
+ const content = readFileSync9(configPath, "utf-8");
3015
3299
  const loaded = JSON.parse(content);
3016
3300
  return {
3017
3301
  removeBg: { ...DEFAULT_CONFIG.removeBg, ...loaded.removeBg },
@@ -3025,8 +3309,8 @@ function loadConfig() {
3025
3309
  function resetConfig() {
3026
3310
  const configPath = getConfigPath();
3027
3311
  const configDir = dirname7(configPath);
3028
- if (!existsSync10(configDir)) {
3029
- mkdirSync6(configDir, { recursive: true });
3312
+ if (!existsSync12(configDir)) {
3313
+ mkdirSync7(configDir, { recursive: true });
3030
3314
  }
3031
3315
  writeFileSync3(configPath, JSON.stringify(DEFAULT_CONFIG, null, 2));
3032
3316
  }
@@ -3129,7 +3413,7 @@ async function run7(inputRaw) {
3129
3413
  return false;
3130
3414
  }
3131
3415
  const input = normalizePath(inputRaw);
3132
- if (!existsSync11(input)) {
3416
+ if (!existsSync13(input)) {
3133
3417
  error(`File not found: ${input}`);
3134
3418
  await pauseOnError();
3135
3419
  return false;
@@ -3152,7 +3436,7 @@ async function run7(inputRaw) {
3152
3436
  }
3153
3437
  async function runGUI8(inputRaw) {
3154
3438
  const input = normalizePath(inputRaw);
3155
- if (!existsSync11(input)) {
3439
+ if (!existsSync13(input)) {
3156
3440
  error(`File not found: ${input}`);
3157
3441
  return false;
3158
3442
  }
@@ -3190,15 +3474,12 @@ async function runGUI8(inputRaw) {
3190
3474
  return generatePreview5(input, borderColor, options);
3191
3475
  },
3192
3476
  onProcess: async (opts) => {
3193
- const logs = [];
3194
3477
  if (!await checkImageMagick()) {
3195
3478
  return {
3196
3479
  success: false,
3197
- error: "ImageMagick not found",
3198
- logs: [{ type: "error", message: "ImageMagick not found. Install with: sudo apt install imagemagick" }]
3480
+ error: "ImageMagick not found"
3199
3481
  };
3200
3482
  }
3201
- logs.push({ type: "info", message: `Processing ${basename9(input)}...` });
3202
3483
  const options = {
3203
3484
  fuzz: opts.fuzz ?? defaults.fuzz,
3204
3485
  doTrim: opts.trim ?? defaults.trim,
@@ -3206,22 +3487,25 @@ async function runGUI8(inputRaw) {
3206
3487
  makeSquare: opts.makeSquare ?? defaults.makeSquare
3207
3488
  };
3208
3489
  const fileInfo = getFileInfo(input);
3209
- const output = `${fileInfo.dirname}/${fileInfo.filename}_nobg.png`;
3210
- logs.push({ type: "info", message: "Removing background..." });
3490
+ const outputExt = fileInfo.extension.toLowerCase() === ".gif" ? ".gif" : ".png";
3491
+ const output = `${fileInfo.dirname}/${fileInfo.filename}_nobg${outputExt}`;
3492
+ const logs = [];
3211
3493
  const success2 = await processImageSilent(input, borderColor, options, logs);
3212
- if (success2) {
3494
+ if (success2 && existsSync13(output)) {
3213
3495
  const finalDims = await getDimensions(output);
3214
3496
  const sizeStr = finalDims ? ` (${finalDims[0]}x${finalDims[1]})` : "";
3497
+ const buffer = readFileSync10(output);
3498
+ const mimeType = outputExt === ".gif" ? "image/gif" : "image/png";
3499
+ const imageData = `data:${mimeType};base64,${buffer.toString("base64")}`;
3215
3500
  return {
3216
3501
  success: true,
3217
3502
  output: `${basename9(output)}${sizeStr}`,
3218
- logs
3503
+ imageData
3219
3504
  };
3220
3505
  }
3221
3506
  return {
3222
3507
  success: false,
3223
- error: "Processing failed",
3224
- logs
3508
+ error: "Processing failed"
3225
3509
  };
3226
3510
  }
3227
3511
  });
@@ -3280,9 +3564,9 @@ async function processImageSilent(input, borderColor, options, logs) {
3280
3564
  async function generatePreview5(input, borderColor, options) {
3281
3565
  const tempDir = tmpdir7();
3282
3566
  const timestamp = Date.now();
3283
- const tempSource = join10(tempDir, `piclet-preview-${timestamp}-src.png`);
3284
- const tempFile = join10(tempDir, `piclet-preview-${timestamp}.png`);
3285
- const tempOutput = join10(tempDir, `piclet-preview-${timestamp}-out.png`);
3567
+ const tempSource = join11(tempDir, `piclet-preview-${timestamp}-src.png`);
3568
+ const tempFile = join11(tempDir, `piclet-preview-${timestamp}.png`);
3569
+ const tempOutput = join11(tempDir, `piclet-preview-${timestamp}-out.png`);
3286
3570
  try {
3287
3571
  let previewInput = input;
3288
3572
  if (isMultiFrame(input)) {
@@ -3310,13 +3594,13 @@ async function generatePreview5(input, borderColor, options) {
3310
3594
  }
3311
3595
  }
3312
3596
  if (options.makeSquare) {
3313
- const squareFile = join10(tempDir, `piclet-preview-${timestamp}-sq.png`);
3597
+ const squareFile = join11(tempDir, `piclet-preview-${timestamp}-sq.png`);
3314
3598
  if (await squarify(currentFile, squareFile)) {
3315
3599
  cleanup(currentFile);
3316
3600
  currentFile = squareFile;
3317
3601
  }
3318
3602
  }
3319
- const buffer = readFileSync9(currentFile);
3603
+ const buffer = readFileSync10(currentFile);
3320
3604
  const base64 = buffer.toString("base64");
3321
3605
  const imageData = `data:image/png;base64,${base64}`;
3322
3606
  const dims = await getDimensions(currentFile);
@@ -3340,9 +3624,9 @@ var config8 = {
3340
3624
  };
3341
3625
 
3342
3626
  // src/tools/rescale.ts
3343
- import { existsSync as existsSync12, readFileSync as readFileSync10 } from "fs";
3627
+ import { existsSync as existsSync14, readFileSync as readFileSync11 } from "fs";
3344
3628
  import { tmpdir as tmpdir8 } from "os";
3345
- import { basename as basename10, join as join11 } from "path";
3629
+ import { basename as basename10, join as join12 } from "path";
3346
3630
  async function run8(inputRaw) {
3347
3631
  if (!await checkImageMagick()) {
3348
3632
  error("ImageMagick not found. Please install it:");
@@ -3351,7 +3635,7 @@ async function run8(inputRaw) {
3351
3635
  return false;
3352
3636
  }
3353
3637
  const input = normalizePath(inputRaw);
3354
- if (!existsSync12(input)) {
3638
+ if (!existsSync14(input)) {
3355
3639
  error(`File not found: ${input}`);
3356
3640
  await pauseOnError();
3357
3641
  return false;
@@ -3412,7 +3696,7 @@ async function run8(inputRaw) {
3412
3696
  } else {
3413
3697
  scaled = await resize(input, output, targetW, targetH);
3414
3698
  }
3415
- if (!scaled || !existsSync12(output)) {
3699
+ if (!scaled || !existsSync14(output)) {
3416
3700
  wipDone(false, "Scaling failed");
3417
3701
  return false;
3418
3702
  }
@@ -3428,7 +3712,7 @@ async function run8(inputRaw) {
3428
3712
  }
3429
3713
  async function runGUI9(inputRaw) {
3430
3714
  const input = normalizePath(inputRaw);
3431
- if (!existsSync12(input)) {
3715
+ if (!existsSync14(input)) {
3432
3716
  error(`File not found: ${input}`);
3433
3717
  return false;
3434
3718
  }
@@ -3482,7 +3766,7 @@ async function runGUI9(inputRaw) {
3482
3766
  } else {
3483
3767
  scaled = await resize(input, output, options.width, options.height);
3484
3768
  }
3485
- if (scaled && existsSync12(output)) {
3769
+ if (scaled && existsSync14(output)) {
3486
3770
  const finalDims = await getDimensions(output);
3487
3771
  const sizeStr = finalDims ? ` (${finalDims[0]}x${finalDims[1]})` : "";
3488
3772
  logs.push({ type: "success", message: "Scaled successfully" });
@@ -3500,8 +3784,8 @@ async function runGUI9(inputRaw) {
3500
3784
  async function generatePreview6(input, options) {
3501
3785
  const tempDir = tmpdir8();
3502
3786
  const timestamp = Date.now();
3503
- const tempSource = join11(tempDir, `piclet-preview-${timestamp}-src.png`);
3504
- const tempOutput = join11(tempDir, `piclet-preview-${timestamp}.png`);
3787
+ const tempSource = join12(tempDir, `piclet-preview-${timestamp}-src.png`);
3788
+ const tempOutput = join12(tempDir, `piclet-preview-${timestamp}.png`);
3505
3789
  try {
3506
3790
  let previewInput = input;
3507
3791
  if (isMultiFrame(input)) {
@@ -3521,11 +3805,11 @@ async function generatePreview6(input, options) {
3521
3805
  } else {
3522
3806
  scaled = await resize(previewInput, tempOutput, targetW, targetH);
3523
3807
  }
3524
- if (!scaled || !existsSync12(tempOutput)) {
3808
+ if (!scaled || !existsSync14(tempOutput)) {
3525
3809
  cleanup(tempSource);
3526
3810
  return { success: false, error: "Scaling failed" };
3527
3811
  }
3528
- const buffer = readFileSync10(tempOutput);
3812
+ const buffer = readFileSync11(tempOutput);
3529
3813
  const base64 = buffer.toString("base64");
3530
3814
  const imageData = `data:image/png;base64,${base64}`;
3531
3815
  const dims = await getDimensions(tempOutput);
@@ -3549,8 +3833,8 @@ var config9 = {
3549
3833
  };
3550
3834
 
3551
3835
  // src/tools/storepack.ts
3552
- import { existsSync as existsSync13, mkdirSync as mkdirSync7 } from "fs";
3553
- import { basename as basename11, join as join12 } from "path";
3836
+ import { existsSync as existsSync15, mkdirSync as mkdirSync8 } from "fs";
3837
+ import { basename as basename11, join as join13 } from "path";
3554
3838
  async function scaleImage(input, output, width, height, mode) {
3555
3839
  switch (mode) {
3556
3840
  case "fill":
@@ -3567,7 +3851,7 @@ async function generatePresetImages(sourceImg, outputDir, preset, scaleMode = "f
3567
3851
  const total = preset.icons.length;
3568
3852
  for (let i = 0; i < total; i++) {
3569
3853
  const icon = preset.icons[i];
3570
- const outputPath = join12(outputDir, icon.filename);
3854
+ const outputPath = join13(outputDir, icon.filename);
3571
3855
  if (logs) {
3572
3856
  logs.push({ type: "info", message: `[${i + 1}/${total}] ${icon.filename}` });
3573
3857
  } else {
@@ -3603,7 +3887,7 @@ async function run9(inputRaw) {
3603
3887
  return false;
3604
3888
  }
3605
3889
  const input = normalizePath(inputRaw);
3606
- if (!existsSync13(input)) {
3890
+ if (!existsSync15(input)) {
3607
3891
  error(`File not found: ${input}`);
3608
3892
  await pauseOnError();
3609
3893
  return false;
@@ -3631,11 +3915,11 @@ async function run9(inputRaw) {
3631
3915
  return false;
3632
3916
  }
3633
3917
  const outputDir = `${fileInfo.dirname}/${fileInfo.filename}_${preset.id}`;
3634
- mkdirSync7(outputDir, { recursive: true });
3918
+ mkdirSync8(outputDir, { recursive: true });
3635
3919
  info(`Output: ${outputDir}`);
3636
3920
  console.log("");
3637
3921
  wip("Preparing source...");
3638
- const tempSource = join12(outputDir, ".source.png");
3922
+ const tempSource = join13(outputDir, ".source.png");
3639
3923
  if (!await squarify(input, tempSource)) {
3640
3924
  wipDone(false, "Failed to prepare source");
3641
3925
  return false;
@@ -3656,7 +3940,7 @@ async function run9(inputRaw) {
3656
3940
  }
3657
3941
  async function runGUI10(inputRaw) {
3658
3942
  const input = normalizePath(inputRaw);
3659
- if (!existsSync13(input)) {
3943
+ if (!existsSync15(input)) {
3660
3944
  error(`File not found: ${input}`);
3661
3945
  return false;
3662
3946
  }
@@ -3705,7 +3989,7 @@ async function runGUI10(inputRaw) {
3705
3989
  };
3706
3990
  }
3707
3991
  const outputDir = `${fileInfo.dirname}/${fileInfo.filename}_${preset.id}`;
3708
- mkdirSync7(outputDir, { recursive: true });
3992
+ mkdirSync8(outputDir, { recursive: true });
3709
3993
  logs.push({ type: "info", message: `Output: ${outputDir}` });
3710
3994
  logs.push({ type: "info", message: `Scale mode: ${scaleMode}` });
3711
3995
  logs.push({ type: "info", message: `Generating ${preset.icons.length} images...` });
@@ -3734,9 +4018,9 @@ var config10 = {
3734
4018
  };
3735
4019
 
3736
4020
  // src/tools/transform.ts
3737
- import { existsSync as existsSync14, readFileSync as readFileSync11 } from "fs";
4021
+ import { existsSync as existsSync16, readFileSync as readFileSync12 } from "fs";
3738
4022
  import { tmpdir as tmpdir9 } from "os";
3739
- import { basename as basename12, join as join13 } from "path";
4023
+ import { basename as basename12, join as join14 } from "path";
3740
4024
  var TRANSFORM_LABELS = {
3741
4025
  "flip-h": "Flip Horizontal",
3742
4026
  "flip-v": "Flip Vertical",
@@ -3812,7 +4096,7 @@ async function run10(inputRaw) {
3812
4096
  return false;
3813
4097
  }
3814
4098
  const input = normalizePath(inputRaw);
3815
- if (!existsSync14(input)) {
4099
+ if (!existsSync16(input)) {
3816
4100
  error(`File not found: ${input}`);
3817
4101
  await pauseOnError();
3818
4102
  return false;
@@ -3831,7 +4115,7 @@ async function run10(inputRaw) {
3831
4115
  }
3832
4116
  async function runGUI11(inputRaw) {
3833
4117
  const input = normalizePath(inputRaw);
3834
- if (!existsSync14(input)) {
4118
+ if (!existsSync16(input)) {
3835
4119
  error(`File not found: ${input}`);
3836
4120
  return false;
3837
4121
  }
@@ -3914,8 +4198,8 @@ async function runGUI11(inputRaw) {
3914
4198
  async function generatePreview7(input, options) {
3915
4199
  const tempDir = tmpdir9();
3916
4200
  const timestamp = Date.now();
3917
- const tempSource = join13(tempDir, `piclet-preview-${timestamp}-src.png`);
3918
- const tempOutput = join13(tempDir, `piclet-preview-${timestamp}.png`);
4201
+ const tempSource = join14(tempDir, `piclet-preview-${timestamp}-src.png`);
4202
+ const tempOutput = join14(tempDir, `piclet-preview-${timestamp}.png`);
3919
4203
  try {
3920
4204
  let previewInput = input;
3921
4205
  if (isMultiFrame(input)) {
@@ -3946,7 +4230,7 @@ async function generatePreview7(input, options) {
3946
4230
  cleanup(tempSource, tempOutput);
3947
4231
  return { success: false, error: "Transform failed" };
3948
4232
  }
3949
- const buffer = readFileSync11(tempOutput);
4233
+ const buffer = readFileSync12(tempOutput);
3950
4234
  const base64 = buffer.toString("base64");
3951
4235
  const imageData = `data:image/png;base64,${base64}`;
3952
4236
  const dims = await getDimensions(tempOutput);
@@ -4003,6 +4287,10 @@ function getToolsForExtension(extension) {
4003
4287
  }
4004
4288
 
4005
4289
  // src/cli/utils.ts
4290
+ import { extname as extname4 } from "path";
4291
+ import { dirname as dirname9 } from "path";
4292
+ import { fileURLToPath as fileURLToPath2 } from "url";
4293
+ import chalk from "chalk";
4006
4294
  function getDistDir() {
4007
4295
  const currentFile = fileURLToPath2(import.meta.url);
4008
4296
  return dirname9(currentFile);
@@ -4014,7 +4302,7 @@ function validateExtensions(files, allowedExtensions) {
4014
4302
  const valid = [];
4015
4303
  const invalid = [];
4016
4304
  for (const file of files) {
4017
- const ext = extname3(file).toLowerCase();
4305
+ const ext = extname4(file).toLowerCase();
4018
4306
  if (allowedExtensions.includes(ext)) {
4019
4307
  valid.push(file);
4020
4308
  } else {
@@ -4074,7 +4362,7 @@ function registerBorderCommand(program2) {
4074
4362
  if (valid.length === 0) {
4075
4363
  process.exit(1);
4076
4364
  }
4077
- const result = await runGUI(valid[0]);
4365
+ const result = await picletTool.runGUI(valid[0]);
4078
4366
  process.exit(result ? 0 : 1);
4079
4367
  }
4080
4368
  const success2 = await runToolOnFiles(
@@ -4118,7 +4406,7 @@ function registerExtractFramesCommand(program2) {
4118
4406
  if (valid.length === 0) {
4119
4407
  process.exit(1);
4120
4408
  }
4121
- const result = await runGUI2(valid[0]);
4409
+ const result = await picletTool.runGUI(valid[0]);
4122
4410
  process.exit(result ? 0 : 1);
4123
4411
  }
4124
4412
  const success2 = await runToolOnFiles(
@@ -4145,7 +4433,7 @@ function registerFilterCommand(program2) {
4145
4433
  if (valid.length === 0) {
4146
4434
  process.exit(1);
4147
4435
  }
4148
- const result = await runGUI3(valid[0]);
4436
+ const result = await picletTool.runGUI(valid[0]);
4149
4437
  process.exit(result ? 0 : 1);
4150
4438
  }
4151
4439
  const success2 = await runToolOnFiles(
@@ -4179,7 +4467,7 @@ function registerIconpackCommand(program2) {
4179
4467
  if (valid.length === 0) {
4180
4468
  process.exit(1);
4181
4469
  }
4182
- const result = await runGUI4(valid[0]);
4470
+ const result = await picletTool.runGUI(valid[0]);
4183
4471
  process.exit(result ? 0 : 1);
4184
4472
  }
4185
4473
  const success2 = await runToolOnFiles(
@@ -4196,7 +4484,7 @@ import chalk7 from "chalk";
4196
4484
 
4197
4485
  // src/lib/registry.ts
4198
4486
  import { exec as exec2 } from "child_process";
4199
- import { existsSync as existsSync15 } from "fs";
4487
+ import { existsSync as existsSync17 } from "fs";
4200
4488
  import { dirname as dirname10 } from "path";
4201
4489
  import { fileURLToPath as fileURLToPath3 } from "url";
4202
4490
  import { promisify as promisify2 } from "util";
@@ -4205,7 +4493,7 @@ function isWSL() {
4205
4493
  return process.platform === "linux" && (process.env.WSL_DISTRO_NAME !== void 0 || process.env.WSLENV !== void 0);
4206
4494
  }
4207
4495
  function isWSLInteropEnabled() {
4208
- return existsSync15("/proc/sys/fs/binfmt_misc/WSLInterop");
4496
+ return existsSync17("/proc/sys/fs/binfmt_misc/WSLInterop");
4209
4497
  }
4210
4498
  async function addRegistryKey(keyPath, valueName, value, type = "REG_SZ") {
4211
4499
  const valueArg = valueName ? `/v "${valueName}"` : "/ve";
@@ -4247,7 +4535,7 @@ async function deleteRegistryKey(keyPath) {
4247
4535
 
4248
4536
  // src/cli/registry.ts
4249
4537
  import { writeFile } from "fs/promises";
4250
- import { join as join14 } from "path";
4538
+ import { join as join15 } from "path";
4251
4539
  async function registerUnifiedMenu(extension, iconsDir, launcherPath) {
4252
4540
  const basePath = `HKCU\\Software\\Classes\\SystemFileAssociations\\${extension}\\shell\\PicLet`;
4253
4541
  const iconsDirWin = wslToWindows(iconsDir);
@@ -4283,8 +4571,8 @@ async function unregisterMenuForExtension(extension) {
4283
4571
  }
4284
4572
  async function registerAllTools() {
4285
4573
  const distDir = getDistDir();
4286
- const iconsDir = join14(distDir, "icons");
4287
- const launcherPath = join14(distDir, "launcher.vbs");
4574
+ const iconsDir = join15(distDir, "icons");
4575
+ const launcherPath = join15(distDir, "launcher.vbs");
4288
4576
  const results = [];
4289
4577
  for (const extension of picletTool.config.extensions) {
4290
4578
  const result = await registerUnifiedMenu(extension, iconsDir, launcherPath);
@@ -4360,8 +4648,8 @@ function escapeRegValue(value) {
4360
4648
  }
4361
4649
  function generateRegContent() {
4362
4650
  const distDir = getDistDir();
4363
- const iconsDir = join14(distDir, "icons");
4364
- const launcherPath = join14(distDir, "launcher.vbs");
4651
+ const iconsDir = join15(distDir, "icons");
4652
+ const launcherPath = join15(distDir, "launcher.vbs");
4365
4653
  const iconsDirWin = wslToWindows(iconsDir);
4366
4654
  const launcherWin = wslToWindows(launcherPath);
4367
4655
  const lines = ["Windows Registry Editor Version 5.00", ""];
@@ -4398,14 +4686,14 @@ function generateUninstallRegContent() {
4398
4686
  }
4399
4687
  async function generateRegFile() {
4400
4688
  const distDir = getDistDir();
4401
- const regPath = join14(distDir, "piclet-install.reg");
4689
+ const regPath = join15(distDir, "piclet-install.reg");
4402
4690
  const content = generateRegContent();
4403
4691
  await writeFile(regPath, content, "utf-8");
4404
4692
  return regPath;
4405
4693
  }
4406
4694
  async function generateUninstallRegFile() {
4407
4695
  const distDir = getDistDir();
4408
- const regPath = join14(distDir, "piclet-uninstall.reg");
4696
+ const regPath = join15(distDir, "piclet-uninstall.reg");
4409
4697
  const content = generateUninstallRegContent();
4410
4698
  await writeFile(regPath, content, "utf-8");
4411
4699
  return regPath;
@@ -4458,9 +4746,22 @@ function registerInstallCommand(program2) {
4458
4746
  chalk7.yellow(`! Registered ${successCount}/${results.length} entries.`)
4459
4747
  );
4460
4748
  }
4461
- console.log(chalk7.bold("\nUsage:"));
4749
+ console.log(chalk7.bold("\nContext Menu Usage:"));
4462
4750
  console.log(" Right-click any supported image in Windows Explorer.");
4463
4751
  console.log(" Multi-select supported for batch processing.");
4752
+ console.log(chalk7.bold("\nCLI Usage:"));
4753
+ console.log(chalk7.cyan(" piclet <image>") + chalk7.dim(" Open GUI editor"));
4754
+ console.log(chalk7.cyan(" piclet makeicon <img>") + chalk7.dim(" Convert to .ico"));
4755
+ console.log(chalk7.cyan(" piclet remove-bg <img>") + chalk7.dim(" Remove background"));
4756
+ console.log(chalk7.cyan(" piclet scale <img>") + chalk7.dim(" Resize image"));
4757
+ console.log(chalk7.cyan(" piclet iconpack <img>") + chalk7.dim(" Generate icon pack"));
4758
+ console.log(chalk7.cyan(" piclet storepack <img>") + chalk7.dim(" Generate store assets"));
4759
+ console.log(chalk7.cyan(" piclet transform <img>") + chalk7.dim(" Rotate/flip image"));
4760
+ console.log(chalk7.cyan(" piclet filter <img>") + chalk7.dim(" Apply filters"));
4761
+ console.log(chalk7.cyan(" piclet border <img>") + chalk7.dim(" Add border"));
4762
+ console.log(chalk7.cyan(" piclet recolor <img>") + chalk7.dim(" Replace colors"));
4763
+ console.log(chalk7.cyan(" piclet extract-frames <gif>") + chalk7.dim(" Extract GIF frames"));
4764
+ console.log(chalk7.dim('\n Run "piclet --help" for full documentation.'));
4464
4765
  console.log();
4465
4766
  });
4466
4767
  }
@@ -4480,7 +4781,7 @@ function registerMakeiconCommand(program2) {
4480
4781
  if (valid.length === 0) {
4481
4782
  process.exit(1);
4482
4783
  }
4483
- const result = await runGUI5(valid[0]);
4784
+ const result = await picletTool.runGUI(valid[0]);
4484
4785
  process.exit(result ? 0 : 1);
4485
4786
  }
4486
4787
  const success2 = await runToolOnFiles(
@@ -4522,7 +4823,7 @@ function registerRecolorCommand(program2) {
4522
4823
  if (valid.length === 0) {
4523
4824
  process.exit(1);
4524
4825
  }
4525
- const result = await runGUI7(valid[0]);
4826
+ const result = await picletTool.runGUI(valid[0]);
4526
4827
  process.exit(result ? 0 : 1);
4527
4828
  }
4528
4829
  const success2 = await runToolOnFiles(
@@ -4549,7 +4850,7 @@ function registerRemoveBgCommand(program2) {
4549
4850
  if (valid.length === 0) {
4550
4851
  process.exit(1);
4551
4852
  }
4552
- const result = await runGUI8(valid[0]);
4853
+ const result = await picletTool.runGUI(valid[0]);
4553
4854
  process.exit(result ? 0 : 1);
4554
4855
  }
4555
4856
  if (options.fuzz !== void 0) {
@@ -4589,7 +4890,7 @@ function registerScaleCommand(program2) {
4589
4890
  if (valid.length === 0) {
4590
4891
  process.exit(1);
4591
4892
  }
4592
- const result = await runGUI9(valid[0]);
4893
+ const result = await picletTool.runGUI(valid[0]);
4593
4894
  process.exit(result ? 0 : 1);
4594
4895
  }
4595
4896
  const success2 = await runToolOnFiles(
@@ -4616,7 +4917,7 @@ function registerStorepackCommand(program2) {
4616
4917
  if (valid.length === 0) {
4617
4918
  process.exit(1);
4618
4919
  }
4619
- const result = await runGUI10(valid[0]);
4920
+ const result = await picletTool.runGUI(valid[0]);
4620
4921
  process.exit(result ? 0 : 1);
4621
4922
  }
4622
4923
  const success2 = await runToolOnFiles(
@@ -4643,7 +4944,7 @@ function registerTransformCommand(program2) {
4643
4944
  if (valid.length === 0) {
4644
4945
  process.exit(1);
4645
4946
  }
4646
- const result = await runGUI11(valid[0]);
4947
+ const result = await picletTool.runGUI(valid[0]);
4647
4948
  process.exit(result ? 0 : 1);
4648
4949
  }
4649
4950
  const success2 = await runToolOnFiles(