cantarella 1.0.0 → 1.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/{Readme.md → README.md} +35 -12
- package/dist/bin/cli.js +1 -1
- package/dist/config.js +1 -1
- package/dist/lib/core/command.js +1 -1
- package/dist/lib/core/compress.js +1 -1
- package/dist/lib/core/compressImage.js +1 -1
- package/dist/lib/core/report.js +2 -2
- package/dist/lib/utils/fs.js +2 -1
- package/dist/lib/utils/theme.js +1 -1
- package/package.json +5 -1
package/{Readme.md → README.md}
RENAMED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
**批量压缩图片的命令行工具**
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
传入一个文件夹或单张图片,默认压缩覆盖(覆盖前把原图备份到备份目录,默认 `.backup/`,可用 `--backup-dir` 自定义);也可用 `--out` 把结果写进独立目录树,原图一个字节都不动。
|
|
8
8
|
|
|
9
9
|
[](https://nodejs.org/)
|
|
10
10
|
[](./LICENSE)
|
|
@@ -16,8 +16,9 @@
|
|
|
16
16
|
|
|
17
17
|
## ✨ 特性
|
|
18
18
|
|
|
19
|
-
- **两种使用方式**:不传参数进入交互问答模式;传参数则为纯 CLI
|
|
20
|
-
-
|
|
19
|
+
- **两种使用方式**:不传参数进入交互问答模式;传参数则为纯 CLI 模式,可写进脚本;`-y` 全默认模式让裸命令也能进脚本
|
|
20
|
+
- **安全第一**:覆盖原文件前自动备份(目录名可用 `--backup-dir` 自定义,带撞名防护,绝不共用你已有的文件夹);重压缩不会变小的文件自动跳过
|
|
21
|
+
- **`--out` 输出模式**:结果写进独立目录树(镜像源目录结构),不动原图、不写备份;压不动的文件原样拷贝进去,目录树永远完整;同名输出冲突(如 heic→jpg 撞已有 a.jpg)被拦截报错而不是静默覆盖
|
|
21
22
|
- **`--dry` 预览**:真实编码计算压缩后大小,但不写任何文件
|
|
22
23
|
- **`--report` 报告**:在目标目录生成 markdown 压缩报告
|
|
23
24
|
- **格式转换**:jpeg / png / webp / avif / tiff / gif 互转,支持 heic(iPhone 照片)与 svg 输入
|
|
@@ -35,7 +36,7 @@ npm install -g cantarella
|
|
|
35
36
|
```bash
|
|
36
37
|
git clone https://github.com/zzl110712/cantarella.git
|
|
37
38
|
pnpm install
|
|
38
|
-
pnpm build #
|
|
39
|
+
pnpm build # 编译并压缩 TS 到 dist/(bin 入口指向编译产物)
|
|
39
40
|
npm link # 注册全局命令 cantarella
|
|
40
41
|
```
|
|
41
42
|
|
|
@@ -52,20 +53,27 @@ cantarella compress ./photos -f webp # 全部转成 webp
|
|
|
52
53
|
cantarella compress ./photos -w 1920 # 限制最大宽度 1920(只缩小不放大)
|
|
53
54
|
cantarella compress ./photos --dry # 预览:真实计算大小但不写任何文件
|
|
54
55
|
cantarella compress ./photos --report # 生成 markdown 压缩报告
|
|
56
|
+
cantarella compress -y # 跳过全部问答,全默认直接压当前目录(脚本友好)
|
|
57
|
+
cantarella compress ./photos --backup-dir bak # 备份到自定义目录 bak/ 而不是 .backup/
|
|
58
|
+
cantarella compress ./photos -o dist # 输出到独立目录 dist/(镜像结构,不动原图)
|
|
55
59
|
```
|
|
56
60
|
|
|
57
61
|
交互模式中直接回车即采用默认值;ESC / Ctrl+C 随时取消。
|
|
58
62
|
|
|
59
63
|
## ⚙️ 选项
|
|
60
64
|
|
|
61
|
-
| 选项 | 说明
|
|
62
|
-
| ------------------------ |
|
|
63
|
-
| `-q, --quality <number>` | 压缩质量 1-100(png 映射为压缩等级)
|
|
64
|
-
| `-f, --format <format>` | 输出格式:`jpeg` `png` `webp` `avif` `tiff` `gif`
|
|
65
|
-
| `-w, --max-width <px>` | 等比缩放的最大宽度,只缩小不放大
|
|
66
|
-
| `-r, --recursive` | 递归处理子目录
|
|
67
|
-
| `--dry` | 预览模式:真实计算压缩后大小,但不写任何文件
|
|
68
|
-
| `--report` | 在目标目录生成 markdown 压缩报告
|
|
65
|
+
| 选项 | 说明 | 默认值 |
|
|
66
|
+
| ------------------------ | ---------------------------------------------------------------------------------------------------------- | --------------------- |
|
|
67
|
+
| `-q, --quality <number>` | 压缩质量 1-100(png 映射为压缩等级) | `80` |
|
|
68
|
+
| `-f, --format <format>` | 输出格式:`jpeg` `png` `webp` `avif` `tiff` `gif` | 保持原格式 |
|
|
69
|
+
| `-w, --max-width <px>` | 等比缩放的最大宽度,只缩小不放大 | `0`(不缩放) |
|
|
70
|
+
| `-r, --recursive` | 递归处理子目录 | 关闭 |
|
|
71
|
+
| `--dry` | 预览模式:真实计算压缩后大小,但不写任何文件 | 关闭 |
|
|
72
|
+
| `--report` | 在目标目录生成 markdown 压缩报告 | 关闭 |
|
|
73
|
+
| `-y, --yes` | 跳过全部问答、直接用默认值开压(裸命令可脚本化) | 关闭 |
|
|
74
|
+
| `--backup-dir <name>` | 备份目录名(须为单纯目录名;非空且非本工具创建的目录会被拒绝使用) | `.backup` |
|
|
75
|
+
| `-c, --concurrency <n>` | 同时处理几张图,1-1024(调大超过 4 时建议同时设 `UV_THREADPOOL_SIZE` 环境变量同值) | `4` |
|
|
76
|
+
| `-o, --out <dir>` | 输出到独立目录(镜像源目录结构,不动原图、不写备份;压不动的文件原样拷贝保证树完整;不能指向目标目录本身) | 无(原地覆盖 + 备份) |
|
|
69
77
|
|
|
70
78
|
## 📋 支持格式
|
|
71
79
|
|
|
@@ -109,6 +117,21 @@ pnpm build # 编译并压缩到 dist/(发布、npm link 前需要)
|
|
|
109
117
|
|
|
110
118
|
## 📜 版本记录
|
|
111
119
|
|
|
120
|
+
### 🚀 1.0.2 · 2026-09-06
|
|
121
|
+
|
|
122
|
+
- ✨ **新增** `-c, --concurrency <n>` 并发选项:CLI 与交互问答双通道,有效范围 1-1024(与 libuv 线程池上限对齐),并发数写入压缩报告的参数行
|
|
123
|
+
- 💡 调大并发(>4)时建议同时设置环境变量 `UV_THREADPOOL_SIZE` 同值——sharp 的编解码跑在 libuv 线程池(默认 4 线程),只调 `-c` 不调线程池不会更快
|
|
124
|
+
- ✨ **新增** `-o, --out <dir>` 输出模式:结果写进独立目录树(`path.relative` + `path.join` 镜像源目录结构,含嵌套子目录),原图零改动、零备份;压不动的文件原样拷贝进目录树(报告标注"原样复制"),目录树永远完整
|
|
125
|
+
- 🛡️ **新增** 输出路径认领集:同名输出冲突(heic→jpg 撞已有 a.jpg、两个文件争同一个输出名)被拦截报错,不再静默"后写者胜";覆盖模式的撞车检测同时收编进同一机制
|
|
126
|
+
- 🛡️ out 模式细节:`--out` 指向目标目录本身会被拒绝(那等于无备份的原地覆盖);`--report` 的报告也写进输出目录,源目录一个字节不动
|
|
127
|
+
|
|
128
|
+
### 🚀 1.0.1 · 2026-09-06
|
|
129
|
+
|
|
130
|
+
- ✨ **新增** `-V, --version`:打印版本号(从包自身的 package.json 动态读取,发版只改一处)
|
|
131
|
+
- ✨ **新增** `-y, --yes` 全默认模式:跳过全部问答、直接用默认值开压,`compress -y` 裸命令也能进脚本;与 `-q` 等选项自由组合(CLI 值优先,其余默认)
|
|
132
|
+
- ✨ **新增** `--backup-dir <name>` 自定义备份目录:目录结构原样重现,扫描时自动忽略;非法名(含路径分隔符、`..`)在参数层被拒绝
|
|
133
|
+
- 🛡️ **新增** 备份目录撞名防护:非空且非 cantarella 创建的自定义目录直接报错拒用(一个文件都不动),杜绝"用户同名文件被误认成已有备份、原图覆盖后最初版丢失"的数据安全问题
|
|
134
|
+
|
|
112
135
|
### 🎉 1.0.0 · 2026-09-04
|
|
113
136
|
|
|
114
137
|
首个版本。
|
package/dist/bin/cli.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
#! /usr/bin/env node
|
|
2
|
-
import{program as r}from"commander";import{myCompress as
|
|
2
|
+
import{createRequire as e}from"node:module";import{program as r}from"commander";import{myCompress as o}from"#lib/core/command";const{version:m}=e(import.meta.url)("#pkg");r.name("cantarella"),r.version(m),o(r),r.parse(process.argv);
|
package/dist/config.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
export default{compress:{extensions:[".jpg",".jpeg",".png",".webp",".avif",".tiff",".gif",".heic",".heif",".svg"],backupDir:".backup",ignoreDirs:[".backup"]}};
|
package/dist/lib/core/command.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{InvalidArgumentError as
|
|
1
|
+
import{InvalidArgumentError as o,Option as t}from"commander";import{compress as e}from"./compress.js";import{OUTPUT_FORMATS as i}from"./compressImage.js";function p(n){const r=Number.parseInt(n,10);if(Number.isNaN(r)||r<1||r>100)throw new o("\u5FC5\u987B\u662F 1 - 100 \u7684\u6574\u6570");return r}function a(n){const r=Number.parseInt(n,10);if(Number.isNaN(r)||r<=0)throw new o("\u5FC5\u987B\u662F\u6B63\u6574\u6570");return r}function s(n){const r=n.trim();if(r===""||r==="."||r===".."||r.includes("/")||r.includes("\\"))throw new o("\u5FC5\u987B\u662F\u5355\u7EAF\u7684\u76EE\u5F55\u540D\uFF08\u4E0D\u80FD\u4E3A\u7A7A\uFF0C\u4E0D\u80FD\u5305\u542B / \\ . ..\uFF09");return r}function c(n){const r=Number.parseInt(n,10);if(Number.isNaN(r)||r<1||r>1024)throw new o("\u5FC5\u987B\u662F 1 - 1024 \u7684\u6574\u6570");return r}function u(n){const r=n.trim();if(r==="")throw new o("\u8F93\u51FA\u8DEF\u5F84\u4E0D\u80FD\u4E3A\u7A7A");return r}const m=new t("-f, --format <format>","\u8F6C\u6362\u8F93\u51FA\u683C\u5F0F\uFF08\u9ED8\u8BA4\u4FDD\u6301\u539F\u683C\u5F0F\uFF09").choices([...i]);export function myCompress(n){n.command("compress [target]").description("\u6279\u91CF\u538B\u7F29\u56FE\u7247\uFF08\u8986\u76D6\u539F\u6587\u4EF6\u524D\u81EA\u52A8\u5907\u4EFD\u5230\u5907\u4EFD\u76EE\u5F55\uFF0C\u9ED8\u8BA4 .backup/\uFF09").option("-q, --quality <number>","\u538B\u7F29\u8D28\u91CF 1-100\uFF0C\u9ED8\u8BA4 80\uFF08png \u6620\u5C04\u4E3A\u538B\u7F29\u7B49\u7EA7\uFF09",p,80).addOption(m).option("-w, --max-width <px>","\u7B49\u6BD4\u7F29\u653E\u7684\u6700\u5927\u5BBD\u5EA6\uFF0C\u53EA\u7F29\u5C0F\u4E0D\u653E\u5927",a).option("-r, --recursive","\u9012\u5F52\u5904\u7406\u5B50\u76EE\u5F55",!1).option("--dry","\u9884\u89C8\u6A21\u5F0F\uFF1A\u771F\u5B9E\u8BA1\u7B97\u538B\u7F29\u540E\u5927\u5C0F\uFF0C\u4F46\u4E0D\u5199\u4EFB\u4F55\u6587\u4EF6",!1).option("--report","\u5728\u76EE\u6807\u76EE\u5F55\u751F\u6210 markdown \u538B\u7F29\u62A5\u544A",!1).option("-y, --yes","\u8DF3\u8FC7\u5168\u90E8\u95EE\u7B54\u3001\u76F4\u63A5\u7528\u9ED8\u8BA4\u503C\u5F00\u538B",!1).option("--backup-dir <name>","\u5907\u4EFD\u76EE\u5F55\u540D\uFF08\u9ED8\u8BA4 .backup\uFF09",s).option("-c, --concurrency <n>","\u540C\u65F6\u5904\u7406\u591A\u5C11\u5F20\u56FE\u7247",c,4).option("-o, --out <dir>","\u8F93\u51FA\u5230\u72EC\u7ACB\u76EE\u5F55\uFF08\u955C\u50CF\u6E90\u76EE\u5F55\u7ED3\u6784\uFF0C\u4E0D\u8986\u76D6\u539F\u56FE\uFF09",u).action(e)}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import*as
|
|
1
|
+
import*as t from"@clack/prompts";import O from"chalk";import R from"p-limit";import l from"node:path";import{mkdir as P}from"node:fs/promises";import{backupDirState as j,collectImages as A,formatBytes as g,formatPercent as W,markBackupDir as L,resolveRoot as U}from"#lib/utils/fs";import{accent as E}from"#lib/utils/theme";import{compressOne as M,OUTPUT_FORMATS as _}from"./compressImage.js";import{buildReport as z,writeReport as G}from"./report.js";import w from"#config";export const compress=async(y,o,q)=>{t.intro(E("Cantarella \u56FE\u7247\u538B\u7F29\u5DE5\u5177"));const p=y===void 0?process.cwd():l.resolve(y.trim());let $=o.recursive??!1,b=o.report??!1;const d=o.backupDir??w.compress.backupDir;if(y===void 0&&!o.yes){const i=r=>q.getOptionValueSource(r)==="cli",a=await t.group({...i("concurrency")?{}:{concurrency:()=>t.text({message:"\u540C\u65F6\u538B\u7F29\u6587\u4EF6\u6570\u91CF",placeholder:"4",defaultValue:"4",validate:r=>{if(r===void 0||r.trim()==="")return;const n=Number(r);if(!Number.isInteger(n)||n<1||n>1024)return"\u8BF7\u8F93\u5165 1 - 1024 \u8303\u56F4\u5185\u7684\u6B63\u6574\u6570"}})},...i("quality")?{}:{quality:()=>t.text({message:"\u538B\u7F29\u8D28\u91CF\uFF081-100\uFF09",placeholder:"80",defaultValue:"80",validate:r=>{if(r===void 0||r.trim()==="")return;const n=Number(r);if(!Number.isInteger(n)||n<1||n>100)return"\u8BF7\u8F93\u5165 1 - 100 \u8303\u56F4\u5185\u7684\u6574\u6570"}})},...i("format")?{}:{format:()=>t.select({message:"\u8F93\u51FA\u683C\u5F0F",options:[{value:"",label:"\u4FDD\u6301\u539F\u683C\u5F0F",hint:"heic => jpeg, svg => png \u81EA\u52A8\u8F6C\u6362"},..._.map(r=>({value:r,label:r}))]})},...i("maxWidth")?{}:{maxWidth:()=>t.text({message:"\u6700\u5927\u5BBD\u5EA6 px\uFF080 = \u4E0D\u7F29\u653E\uFF09",placeholder:"0",defaultValue:"0",validate:r=>{if(r===void 0||r.trim()==="")return;const n=Number(r);if(!Number.isInteger(n)||n<0)return"\u8BF7\u8F93\u5165\u4E0D\u5C0F\u4E8E 0 \u7684\u6574\u6570"}})},...i("recursive")?{}:{recursive:()=>t.confirm({message:"\u9012\u5F52\u5904\u7406\u5B50\u76EE\u5F55\uFF1F",initialValue:!1})},...i("report")?{}:{report:()=>t.confirm({message:"\u751F\u6210 markdown \u538B\u7F29\u62A5\u544A\uFF1F",initialValue:!1})}},{onCancel:()=>{t.cancel("\u7528\u6237\u53D6\u6D88\u64CD\u4F5C"),process.exit(0)}});a.concurrency!==void 0&&(o.concurrency=Number(a.concurrency)),a.quality!==void 0&&(o.quality=Number(a.quality)),a.format!==void 0&&(o.format=a.format),a.maxWidth!==void 0&&Number(a.maxWidth)>0&&(o.maxWidth=Number(a.maxWidth)),a.recursive!==void 0&&($=a.recursive),a.report!==void 0&&(b=a.report)}const S=o.quality??80,k=o.concurrency??4,T=o.format===void 0||o.format===""?void 0:o.format,V=o.maxWidth,m=o.dry??!1,c=o.out!==void 0?l.resolve(o.out):void 0,u=t.spinner();try{u.start("\u6B63\u5728\u626B\u63CF\u6587\u4EF6...");let i="",a=!1;try{const e=await U(p);i=e.root,a=e.isFile}catch{u.error(`\u8DEF\u5F84\u4E0D\u5B58\u5728\u6216\u65E0\u6CD5\u8BBF\u95EE\uFF1A${p}`),process.exitCode=1;return}if(c!==void 0&&c===i){u.error("--out \u4E0D\u80FD\u6307\u5411\u76EE\u6807\u76EE\u5F55\u672C\u8EAB\uFF08\u90A3\u7B49\u4E8E\u65E0\u5907\u4EFD\u8986\u76D6\u539F\u56FE\uFF09"),process.exitCode=1;return}let r;if(a){const e=l.extname(p).toLowerCase();if(!w.compress.extensions.includes(e)){u.error(`\u4E0D\u652F\u6301\u7684\u56FE\u7247\u7C7B\u578B\uFF1A${e===""?"\uFF08\u65E0\u6269\u5C55\u540D\uFF09":e}`),process.exitCode=1;return}r=[p]}else r=await A(i,$,d);if(r.length===0){u.stop(),t.outro("\u6CA1\u6709\u627E\u5230\u53EF\u5904\u7406\u7684\u56FE\u7247");return}if(c===void 0){const e=await j(i,d);if(e==="foreign"&&d!==w.compress.backupDir){u.error(`\u5907\u4EFD\u76EE\u5F55 ${d} \u5DF2\u5B58\u5728\u4E14\u5305\u542B\u975E cantarella \u521B\u5EFA\u7684\u6587\u4EF6\uFF0C\u4E3A\u907F\u514D\u8986\u76D6\u6216\u6DF7\u6DC6\u5DF2\u505C\u6B62\uFF08\u672A\u6539\u52A8\u4EFB\u4F55\u6587\u4EF6\uFF09\u3002\u8BF7\u6362\u4E00\u4E2A --backup-dir \u540D\u5B57\uFF0C\u6216\u5148\u5904\u7406\u8BE5\u76EE\u5F55`),process.exitCode=1;return}e!=="ours"&&!m&&await L(i,d)}const n={quality:S,format:T,maxWidth:V,dry:m,backupDir:d,outDir:c},F=R(k),I=new Set(r);let h=0;u.message(`${m?"\u9884\u89C8":"\u538B\u7F29"} ${h}/${r.length}`);const f=await Promise.all(r.map(e=>F(async()=>{const s=await M(e,n,i,I);return h+=1,u.message(`${m?"\u9884\u89C8":"\u538B\u7F29"} ${h}/${r.length} ${l.basename(e)}`),s}))),v=f.filter(e=>e.status==="skipped"),x=f.filter(e=>e.status==="failed"),C=f.reduce((e,s)=>e+s.beforeBytes,0),N=f.reduce((e,s)=>e+(s.status==="failed"?s.beforeBytes:s.afterBytes),0),B=[`${m?"\u9884\u89C8":"\u538B\u7F29"}\u5B8C\u6210\uFF1A${f.length} \u4E2A\u6587\u4EF6`,`${g(C)} \u2192 ${g(N)}`,`\u8282\u7701 ${W(C,N)}`];if(v.length>0&&B.push(`\u8DF3\u8FC7 ${v.length}`),u.stop(),t.log.success(B.join(",")),x.length>0){t.log.error(`\u5931\u8D25 ${x.length} \u4E2A\uFF1A`);for(const e of x)t.log.error(`${l.relative(i,e.file)} \u2014\u2014 ${e.error??"\u672A\u89E3\u6790\u51FA\u53D1\u751F\u4E86\u4EC0\u4E48"}`);process.exitCode=1}if(m){t.log.message(O.bold("\u9884\u89C8\u660E\u7EC6\uFF08\u672A\u5199\u5165\u4EFB\u4F55\u6587\u4EF6\uFF09\uFF1A"));for(const e of f){const s=l.relative(i,e.file),D=c!==void 0&&e.status!=="failed"?` -> ${l.relative(c,e.output)}`:"";e.status==="failed"?t.log.error(`${s} \u2014\u2014 ${e.error}`):e.status==="skipped"?t.log.warn(c===void 0?`${s} \u4F1A\u8DF3\u8FC7\uFF08\u91CD\u538B\u7F29\u4E0D\u4F1A\u53D8\u5C0F\uFF09`:`${s}${D} \u4E0D\u538B\u7F29\u3001\u539F\u6837\u590D\u5236\uFF08\u91CD\u538B\u7F29\u4E0D\u4F1A\u53D8\u5C0F\uFF09`):t.log.success(`${s}${D} ${g(e.beforeBytes)} => ${g(e.afterBytes)}\uFF08\u8282\u7701 ${W(e.beforeBytes,e.afterBytes)}\uFF09`)}b&&t.log.warn("--dry \u6A21\u5F0F\u627F\u8BFA\u4E0D\u4F1A\u8F93\u51FA\u4EFB\u4F55\u6587\u4EF6\uFF0C\u5DF2\u5FFD\u7565 --report"),t.outro("\u9884\u89C8\u5B8C\u6210");return}if(b){const e={target:p,root:i,recursive:$,concurrency:k,params:n,startedAt:new Date};c!==void 0&&await P(c,{recursive:!0});const s=await G(c??i,z(e,f));t.log.info(`\u62A5\u544A\u5DF2\u751F\u6210\uFF1A${s}`),t.outro("\u5B8C\u6210")}}catch(i){u.stop(),t.log.error(i instanceof Error?i.message:String(i)),process.exitCode=1}};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import
|
|
1
|
+
import j from"sharp";import p from"path";import{randomUUID as y}from"crypto";import{readFile as k,rename as L,unlink as _,writeFile as l,mkdir as x}from"fs/promises";import{backupFile as B,replaceExt as v}from"#lib/utils/fs";export const OUTPUT_FORMATS=["jpeg","png","webp","avif","tiff","gif"];const I=["heif","svg"],O={jpeg:".jpg",png:".png",webp:".webp",avif:".avif",tiff:".tiff",gif:".gif"},U={heif:"jpeg",svg:"png"},A={".jpg":"jpeg",".jpeg":"jpeg",".png":"png",".webp":"webp",".avif":"avif",".tiff":"tiff",".gif":"gif",".heic":"heif",".heif":"heif",".svg":"svg"};function D(t){return OUTPUT_FORMATS.includes(t)}function E(t){const e=Math.round((100-t)/100*9);return t<=60?{compressionLevel:e,palette:!0}:{compressionLevel:e}}function M(t,e,i){switch(e){case"jpeg":return t.jpeg({quality:i,mozjpeg:!0});case"png":return t.png(E(i));case"webp":return t.webp({quality:i});case"avif":return t.avif({quality:i});case"tiff":return t.tiff({quality:i});case"gif":return t.gif()}}export async function compressOne(t,e,i,m){const n={file:t,output:t,format:"jpeg",beforeBytes:0,afterBytes:0,status:"failed"};try{const r=await k(t);n.beforeBytes=r.byteLength;const g=p.extname(t).toLowerCase(),a=A[g];if(a===void 0)throw new Error(`\u4E0D\u652F\u6301\u7684\u56FE\u7247\u6269\u5C55\u540D\uFF1A${g===""?"\u65E0\u6269\u5C55\u540D":g}`);const d=await j(r).metadata(),f=e.format??(D(a)?a:U[a]),c=f===a,b=p.relative(i,t),F=c?b:v(b,O[f]),o=e.outDir!==void 0?p.join(e.outDir,F):c?t:v(t,O[f]);n.output=o,n.format=f;let u=j(r);e.maxWidth!==void 0&&d.width!==void 0&&d.width>e.maxWidth&&(u=u.resize({width:e.maxWidth,withoutEnlargement:!0})),f==="jpeg"&&d.hasAlpha&&u.flatten({background:"#ffffff"}),u=M(u,f,e.quality);const{data:h}=await u.toBuffer({resolveWithObject:!0});n.afterBytes=h.byteLength;const s=c&&h.byteLength>=r.byteLength;if(s&&e.outDir===void 0)return n.status="skipped",n;if(s&&(n.afterBytes=r.byteLength),o!==t&&m.has(o))throw new Error(`\u8F93\u51FA\u8DEF\u5F84\u51B2\u7A81\uFF1A${p.basename(o)} \u5DF2\u88AB\u672C\u6279\u5176\u4ED6\u6587\u4EF6\u5360\u7528`);if(m.add(o),e.dry)return n.status=s?"skipped":"done",n;c&&e.outDir===void 0&&await B(i,t,e.backupDir),e.outDir!==void 0&&await x(p.dirname(o),{recursive:!0});const w=`${o}${y()}.tmp`;try{await l(w,s?r:h),await L(w,o)}catch(T){throw await _(w).catch(()=>{}),T}return n.status=s?"skipped":"done",n}catch(r){return n.error=r instanceof Error?r.message:String(r),n}}
|
package/dist/lib/core/report.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{writeFile as
|
|
2
|
-
`)}async function
|
|
1
|
+
import{writeFile as g}from"fs/promises";import $ from"path";import{formatBytes as u,formatPercent as f}from"#lib/utils/fs";function B(e){const r=o=>String(o).padStart(2,"0");return`${e.getFullYear()}${r(e.getMonth()+1)}${r(e.getDate())}-${r(e.getHours())}${r(e.getMinutes())}${r(e.getSeconds())}`}export function buildReport(e,r){const o=r.filter(t=>t.status==="done"),c=r.filter(t=>t.status==="skipped"),l=r.filter(t=>t.status==="failed"),p=r.reduce((t,a)=>t+a.beforeBytes,0),i=r.reduce((t,a)=>t+(a.status==="failed"?a.beforeBytes:a.afterBytes),0),s=[];s.push("# \u56FE\u7247\u538B\u7F29\u62A5\u544A",""),s.push(`- \u8FD0\u884C\u65F6\u95F4\uFF1A${e.startedAt.toLocaleString("zh-CN",{hour12:!1})}`),s.push(`- \u76EE\u6807\uFF1A${e.target}${e.recursive?"\uFF08\u9012\u5F52\uFF09":""}`),s.push(`- \u53C2\u6570\uFF1Aquality=${e.params.quality} format=${e.params.format??"\u4FDD\u6301\u539F\u683C\u5F0F"} maxWidth=${e.params.maxWidth??"\u4E0D\u7F29\u653E"} \u8F93\u51FA=${e.params.outDir??"\u539F\u5730\u8986\u76D6"} \u5E76\u53D1=${e.concurrency}${e.params.dry?"\uFF08dry \u9884\u89C8\uFF0C\u672A\u5199\u5165\uFF09":""}`,""),s.push("## \u6C47\u603B",""),s.push("| \u6307\u6807 | \u503C |","|---|---|"),s.push(`| \u5904\u7406\u6587\u4EF6\u6570 | ${r.length} |`),s.push(`| \u6210\u529F / \u8DF3\u8FC7 / \u5931\u8D25 | ${o.length} / ${c.length} / ${l.length} |`),s.push(`| \u538B\u7F29\u524D\u603B\u5927\u5C0F | ${u(p)} |`),s.push(`| \u538B\u7F29\u540E\u603B\u5927\u5C0F | ${u(i)} |`),s.push(`| \u8282\u7701 | ${u(p-i)}\uFF08${f(p,i)}\uFF09 |`,""),s.push("## \u660E\u7EC6",""),s.push("| \u6587\u4EF6 | \u8F93\u51FA\u683C\u5F0F | \u539F\u5927\u5C0F | \u8F93\u51FA\u4F4D\u7F6E | \u538B\u7F29\u540E | \u8282\u7701 | \u72B6\u6001 |"),s.push("|---|---|---|---|---|---|---|");for(const t of r){const a=$.relative(e.root,t.file),h=t.status==="failed"?"-":u(t.afterBytes),d=t.status==="failed"?"-":f(t.beforeBytes,t.afterBytes);let n=t.status;t.status==="failed"?n=`failed: ${t.error??"\u672A\u77E5\u9519\u8BEF"}`:t.status==="skipped"&&(n=e.params.outDir!==void 0?"skipped\uFF08\u539F\u6837\u590D\u5236\uFF0C\u672A\u538B\u7F29\uFF09":"skipped\uFF08\u539F\u56FE\u672A\u52A8\uFF09");const m=e.params.outDir??e.root,y=t.status==="failed"?"-":$.relative(m,t.output);s.push(`| ${a} | ${t.format} | ${u(t.beforeBytes)} | ${y} | ${h} | ${d} | ${n} |`)}return s.push(""),s.join(`
|
|
2
|
+
`)}export async function writeReport(e,r){const o=$.join(e,`compress-report-${B(new Date)}.md`);return await g(o,r,"utf-8"),o}
|
package/dist/lib/utils/fs.js
CHANGED
|
@@ -1 +1,2 @@
|
|
|
1
|
-
import{readdir as f,copyFile as
|
|
1
|
+
import{readdir as f,copyFile as w,mkdir as m,stat as c,writeFile as F}from"fs/promises";import{constants as h}from"fs";import n from"path";import i from"#config";export function formatBytes(t){if(t<1024)return`${t} B`;const r=t/1024;if(r<1024)return`${r.toFixed(2)} KB`;const e=r/1024;return e<1024?`${e.toFixed(2)} MB`:`${(e/1024).toFixed(2)} GB`}export function formatPercent(t,r){return t<=0?"-":`${((1-r/t)*100).toFixed(1)}%`}export async function resolveRoot(t){return(await c(t)).isFile()?{root:n.dirname(t),isFile:!0}:{root:t,isFile:!1}}export async function collectImages(t,r,e=i.compress.backupDir){const o=await f(t,{withFileTypes:!0,recursive:r}),s=e&&!i.compress.ignoreDirs.includes(e)?[...i.compress.ignoreDirs,e]:i.compress.ignoreDirs,u=[];for(const a of o){if(!a.isFile())continue;const p=n.join(a.parentPath,a.name),d=n.extname(a.name).toLowerCase();i.compress.extensions.includes(d)&&(r&&n.relative(t,p).split(n.sep).some(x=>s.includes(x))||u.push(p))}return u.sort()}export function backupPathFor(t,r,e=i.compress.backupDir){return n.join(t,e,n.relative(t,r))}export async function backupFile(t,r,e=i.compress.backupDir){const o=backupPathFor(t,r,e);await m(n.dirname(o),{recursive:!0});try{return await w(r,o,h.COPYFILE_EXCL),!0}catch(s){if(s instanceof Error&&s.code==="EEXIST")return!1;throw s}}const l=".cantarella-backup";export async function backupDirState(t,r){const e=n.join(t,r);try{return await c(n.join(e,l)),"ours"}catch{}let o;try{o=await c(e)}catch{return"absent"}return o.isDirectory()&&(await f(e)).length===0?"empty":"foreign"}export async function markBackupDir(t,r){const e=n.join(t,r);await m(e,{recursive:!0});try{await F(n.join(e,l),`cantarella \u5907\u4EFD\u76EE\u5F55\u6807\u8BB0\uFF1A\u672C\u76EE\u5F55\u7531 cantarella \u521B\u5EFA\u5E76\u7BA1\u7406\uFF0C\u8BF7\u52FF\u624B\u52A8\u6DF7\u5165\u5176\u4ED6\u6587\u4EF6\u3002
|
|
2
|
+
`,{flag:"wx"})}catch(o){if(o instanceof Error&&o.code==="EEXIST")return;throw o}}export function replaceExt(t,r){const e=n.basename(t,n.extname(t));return n.join(n.dirname(t),e+r)}
|
package/dist/lib/utils/theme.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import c from"chalk";const
|
|
1
|
+
import c from"chalk";export const accent=c.hex("#0077BE");
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cantarella",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.2",
|
|
4
4
|
"description": "批量压缩图片的命令行工具",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -49,6 +49,10 @@
|
|
|
49
49
|
"#lib/*": {
|
|
50
50
|
"development": "./lib/*.ts",
|
|
51
51
|
"default": "./dist/lib/*.js"
|
|
52
|
+
},
|
|
53
|
+
"#pkg": {
|
|
54
|
+
"development": "./package.json",
|
|
55
|
+
"default": "./package.json"
|
|
52
56
|
}
|
|
53
57
|
},
|
|
54
58
|
"devDependencies": {
|