cantarella 1.2.0 → 1.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -6,6 +6,8 @@
6
6
 
7
7
  两个子命令:`compress` 压缩(默认覆盖原文件,覆盖前自动备份到 `.backup/`,可用 `--backup-dir` 自定义;或用 `--out` 写进独立目录树,原图一个字节都不动);`watermark` 批量加水印(图片 logo 或文字,九宫格定位、平铺防盗图、可调倾斜角)。
8
8
 
9
+ 嫌名字长?装好就有短写:`ct c` = `cantarella compress`,`ct wm` = `cantarella watermark`。
10
+
9
11
  [![Node.js](https://img.shields.io/badge/node-%E2%89%A522.12-brightgreen)](https://nodejs.org/)
10
12
  [![License: ISC](https://img.shields.io/badge/License-ISC-blue.svg)](./LICENSE)
11
13
  [![TypeScript](https://img.shields.io/badge/TypeScript-zero--build-3178C6)](https://www.typescriptlang.org/)
@@ -24,6 +26,7 @@
24
26
  - **`--dry` 预览**:真实编码计算压缩后大小,但不写任何文件
25
27
  - **`--report` 报告**:在目标目录生成 markdown 压缩报告
26
28
  - **格式转换**:jpeg / png / webp / avif / tiff / gif 互转,支持 heic(iPhone 照片)与 svg 输入
29
+ - **PNG 档位压缩**:`-q` 映射到调色板档位(256 / 16 / 4 / 2 色台阶,档内用抖动强度微调);`-q 81+` 走真彩无损重编码(zlib 顶格 + 自适应行过滤)
27
30
  - **并发压缩**:基于 libuv 线程池的 4 路并发 + p-limit 控制
28
31
  - **发布纯 JS**:npm 包只含编译后的 JS,不携带 TS 源码;开发时 Node 原生跑 TS,零构建
29
32
 
@@ -33,13 +36,15 @@
33
36
  npm install -g cantarella
34
37
  ```
35
38
 
39
+ 同时注册 `cantarella` 全名与 `ct` 短名两个全局命令,随你选用。
40
+
36
41
  或从源码运行:
37
42
 
38
43
  ```bash
39
44
  git clone https://github.com/zzl110712/cantarella.git
40
45
  pnpm install
41
46
  pnpm build # 编译并压缩 TS 到 dist/(bin 入口指向编译产物)
42
- npm link # 注册全局命令 cantarella
47
+ npm link # 注册全局命令 cantarella / ct(改过 bin 字段需重跑一次)
43
48
  ```
44
49
 
45
50
  > 要求 Node.js ≥ 22.12.0(npm 包为编译后的纯 JS,无需构建)
@@ -47,6 +52,10 @@ npm link # 注册全局命令 cantarella
47
52
  ## 🚀 用法
48
53
 
49
54
  ```bash
55
+ # 短写:ct c = cantarella compress,ct wm = cantarella watermark,下文示例同义可替换
56
+ ct c ./photos -q 60
57
+ ct wm ./photos -t "© LeoZhao"
58
+
50
59
  cantarella compress # 交互模式:逐项问答
51
60
  cantarella compress ./photos # 压缩目录下所有图片(仅当前层)
52
61
  cantarella compress ./photos -r # 递归子目录
@@ -71,11 +80,11 @@ cantarella watermark ./photos -t "x" --dry # 预览:真实合成但不写
71
80
 
72
81
  ## ⚙️ 选项
73
82
 
74
- ### compress
83
+ ### compress(别名 `c`)
75
84
 
76
85
  | 选项 | 说明 | 默认值 |
77
86
  | ------------------------ | ---------------------------------------------------------------------------------------------------------- | --------------------- |
78
- | `-q, --quality <number>` | 压缩质量 1-100(png 映射为压缩等级) | `80` |
87
+ | `-q, --quality <number>` | 压缩质量 1-100(png 映射为调色板颜色数) | `80` |
79
88
  | `-f, --format <format>` | 输出格式:`jpeg` `png` `webp` `avif` `tiff` `gif` | 保持原格式 |
80
89
  | `-w, --max-width <px>` | 等比缩放的最大宽度,只缩小不放大 | `0`(不缩放) |
81
90
  | `-r, --recursive` | 递归处理子目录 | 关闭 |
@@ -87,7 +96,7 @@ cantarella watermark ./photos -t "x" --dry # 预览:真实合成但不写
87
96
  | `-o, --out <dir>` | 输出到独立目录(镜像源目录结构,不动原图、不写备份;压不动的文件原样拷贝保证树完整;不能指向目标目录本身) | 无(原地覆盖 + 备份) |
88
97
  | `-s, --smart` | 智能模式:每张图在原格式与 webp 间实测选最小(png 额外加赛无损 webp);与 `-f` 互斥 | 关闭 |
89
98
 
90
- ### watermark
99
+ ### watermark(别名 `wm`)
91
100
 
92
101
  | 选项 | 说明 | 默认值 |
93
102
  | ------------------------ | ----------------------------------------------------------------------------------------------------------- | --------------------- |
@@ -115,15 +124,26 @@ cantarella watermark ./photos -t "x" --dry # 预览:真实合成但不写
115
124
 
116
125
  ## 💡 PNG 压缩提示
117
126
 
118
- PNG 是无损格式,默认 `-q 80` 只做无损重编码——对已经优化过的 PNG 基本压不动(会被自动跳过)。想真正压小 PNG:
127
+ PNG 没有平滑的有损 quality,`-q` 被映射到调色板档位上——颜色数决定体积台阶(索引位宽 8/4/2/1bpp),档内用抖动强度微调画质与字节:
128
+
129
+ | quality | 档位 | 效果 |
130
+ | ------- | ---- | ---- |
131
+ | 81-100 | 真彩无损重编码 | zlib 顶格压缩 + 自适应行过滤,像素级保真(编码慢 4-6 倍) |
132
+ | 70-80 | 256 色量化 + 全抖动 | 近无损,默认 `-q 80` 即此档 |
133
+ | 40-69 | 16 色量化 | 体积第一次断崖;档内抖动随 quality 递减 |
134
+ | 10-39 | 4 色量化 | 再腰斩,观感受损明显 |
135
+ | 1-9 | 黑白二值 | 1bpp 极限 |
119
136
 
120
137
  ```bash
121
- cantarella compress ./images -q 60 # 开启 palette 量化(≤256 色),典型节省 80%+
138
+ cantarella compress ./images -q 80 # 默认档:256 色近无损量化
139
+ cantarella compress ./images -q 60 # 16 色:截图 / UI 图形的甜点档
140
+ cantarella compress ./images -q 85 # 无损真彩:像素级保真
141
+ cantarella compress ./images -s # smart 模式:无损 png 与无损 webp 同场比价
122
142
  cantarella compress ./images -f webp # 照片类收益最大
123
143
  ```
124
144
 
125
145
  - **截图 / UI 图形**(大面积纯色):`-q 60` 效果极好
126
- - **照片**存 PNG:palette 容易出色带,建议转 `webp` 或 `avif`
146
+ - **照片**存 PNG:默认 `-q 80` 的 256 色 + 全抖动已可放心用;色带敏感就用 `-q 85` 走无损
127
147
 
128
148
  ## 🛠️ 技术栈
129
149
 
@@ -146,6 +166,18 @@ pnpm build # 编译并压缩到 dist/(发布、npm link 前需要)
146
166
 
147
167
  ## 📜 版本记录
148
168
 
169
+ ### 🚀 1.3.1 · 2026-09-20
170
+
171
+ - ✨ **新增** 全局短命令 `ct`:`bin` 里给同一入口多挂一个名字,安装后 `cantarella` / `ct` 两个命令并存随你选用
172
+ - ✨ **新增** 子命令别名:`compress` 可写作 `c`、`watermark` 可写作 `wm`,最短形态 `ct c ./photos -q 60`;help 的用法行自动显示 `compress|c`
173
+ - 💡 别名是子命令级(永远跟在主命令后输入),不与系统命令冲突;已有全局安装需重跑一次 `npm i -g cantarella` 才会生成 `ct`
174
+
175
+ ### 🚀 1.3.0 · 2026-09-16
176
+
177
+ - ✨ **重构** PNG 的 `-q` 语义为档位表:旧公式"quality 越低压缩等级越高"存在语义倒置(`-q 100` 会算出 cl0 = 完全不压缩),且 `quality <= 60` 才开 palette 的门槛太高;现在 quality 直接踩在 PNG 的位深台阶上(256 / 16 / 4 / 2 色),档内用抖动强度微调画质与字节
178
+ - ✨ **新增** `-q 81+` 真彩无损重编码:`compressionLevel` 恒定 9(zlib 无损、压多狠都不损画质,没有理由不顶格)+ `adaptiveFiltering` 自适应行过滤,实测照片类比旧写法再小 20-40%(代价是编码慢 4-6 倍,与 avif 同量级的取舍)
179
+ - 💡 默认 `-q 80` 从"无损重编码"变为"256 色近无损量化 + 全抖动":已优化过的 PNG 默认档就能压动,照片类典型节省 80% 上下
180
+
149
181
  ### 🚀 1.2.0 · 2026-09-15
150
182
 
151
183
  - ✨ **新增** `watermark` 子命令:批量给图片加水印——图片 logo(`-m`)或文字(`-t`)二选一;九宫格定位(`-p`)、不透明度(`--opacity`)、大小按主图宽百分比(`-s`,图片默认 15 / 文字默认 5)
@@ -1 +1 @@
1
- import{InvalidArgumentError as r,Option as t}from"commander";import{compress as a}from"./compress.js";import{OUTPUT_FORMATS as p}from"./compressImage.js";import{watermark as s}from"./watermark.js";import{WATERMARK_POSITIONS as c}from"./watermarkImage.js";function m(n){const o=Number.parseInt(n,10);if(Number.isNaN(o)||o<1||o>100)throw new r("\u5FC5\u987B\u662F 1 - 100 \u7684\u6574\u6570");return o}function u(n){const o=Number.parseInt(n,10);if(Number.isNaN(o)||o<=0)throw new r("\u5FC5\u987B\u662F\u6B63\u6574\u6570");return o}function e(n){const o=n.trim();if(o===""||o==="."||o===".."||o.includes("/")||o.includes("\\"))throw new r("\u5FC5\u987B\u662F\u5355\u7EAF\u7684\u76EE\u5F55\u540D\uFF08\u4E0D\u80FD\u4E3A\u7A7A\uFF0C\u4E0D\u80FD\u5305\u542B / \\ . ..\uFF09");return o}function i(n){const o=Number.parseInt(n,10);if(Number.isNaN(o)||o<1||o>1024)throw new r("\u5FC5\u987B\u662F 1 - 1024 \u7684\u6574\u6570");return o}function f(n){const o=n.trim();if(o==="")throw new r("\u8F93\u51FA\u8DEF\u5F84\u4E0D\u80FD\u4E3A\u7A7A");return o}function N(n){const o=Number(n);if(Number.isNaN(o)||o<.1||o>1)throw new r("\u5FC5\u987B\u662F 0.1 - 1 \u4E4B\u95F4\u7684\u5C0F\u6570");return o}function w(n){const o=Number.parseInt(n,10);if(Number.isNaN(o)||o<1||o>100)throw new r("\u5FC5\u987B\u662F 1 - 100 \u7684\u6574\u6570");return o}function d(n){const o=Number.parseInt(n,10);if(Number.isNaN(o)||o<-89||o>89)throw new r("\u5FC5\u987B\u662F -89 - 89 \u7684\u6574\u6570\uFF080 = \u4E0D\u503E\u659C\uFF09");return o}const l=new t("-f, --format <format>","\u8F6C\u6362\u8F93\u51FA\u683C\u5F0F\uFF08\u9ED8\u8BA4\u4FDD\u6301\u539F\u683C\u5F0F\uFF09").choices([...p]);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",m,80).addOption(l).option("-w, --max-width <px>","\u7B49\u6BD4\u7F29\u653E\u7684\u6700\u5927\u5BBD\u5EA6\uFF0C\u53EA\u7F29\u5C0F\u4E0D\u653E\u5927",u).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",e).option("-c, --concurrency <n>","\u540C\u65F6\u5904\u7406\u591A\u5C11\u5F20\u56FE\u7247",i,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",f).option("-s, --smart","\u6BCF\u5F20\u56FE\u5206\u522B\u6309\u591A\u4E2A\u5019\u9009\u683C\u5F0F\u7F16\u7801",!1).action(a)}const b=new t("-p, --position <pos>","\u6C34\u5370\u4F4D\u7F6E\uFF08\u4E5D\u5BAB\u683C\u7F29\u5199\uFF09").choices([...c]).default("br");export function myWatermark(n){n.command("watermark [target]").description("\u6279\u91CF\u7ED9\u56FE\u7247\u52A0\u6C34\u5370\uFF1A\u56FE\u7247 logo \u6216\u6587\u5B57\uFF0C\u652F\u6301\u4E5D\u5BAB\u683C\u5B9A\u4F4D\u4E0E\u5E73\u94FA\u9632\u76D7\u56FE").option("-m, --mark <file>","\u6C34\u5370\u56FE\u7247\u8DEF\u5F84\uFF08\u4E0E -t \u4E92\u65A5\uFF09").option("-t, --text <string>","\u6C34\u5370\u6587\u5B57\uFF08\u4E0E -m \u4E92\u65A5\uFF09").addOption(b).option("--opacity <0.1-1>","\u4E0D\u900F\u660E\u5EA6\uFF0C\u9ED8\u8BA4 0.5",N,.5).option("-s, --scale <1-100>","\u6C34\u5370\u5927\u5C0F = \u4E3B\u56FE\u5BBD\u7684\u767E\u5206\u6BD4\uFF08\u9ED8\u8BA4\uFF1A\u56FE\u7247 15\uFF0C\u6587\u5B57 5\uFF09",w).option("--tile","\u5E73\u94FA\u6574\u5F20\u56FE\uFF08\u9632\u76D7\u56FE\uFF09",!1).option("-r, --recursive","\u9012\u5F52\u5904\u7406\u5B50\u76EE\u5F55",!1).option("--dry","\u9884\u89C8\u6A21\u5F0F\uFF1A\u771F\u5B9E\u5408\u6210\uFF0C\u4F46\u4E0D\u5199\u4EFB\u4F55\u6587\u4EF6",!1).option("-y, --yes","\u8DF3\u8FC7\u5168\u90E8\u95EE\u7B54\u3001\u76F4\u63A5\u7528\u9ED8\u8BA4\u503C",!1).option("--backup-dir <name>","\u5907\u4EFD\u76EE\u5F55\u540D\uFF08\u9ED8\u8BA4 .backup\uFF09",e).option("-c, --concurrency <n>","\u540C\u65F6\u5904\u7406\u591A\u5C11\u5F20\u56FE\u7247",i,4).option("-a, --angle <deg>","\u5E73\u94FA\u6C34\u5370\u7684\u503E\u659C\u89D2\u5EA6\uFF08\u5EA6\uFF0C0 = \u4E0D\u503E\u659C\uFF09",d).action(s)}
1
+ import{InvalidArgumentError as r,Option as t}from"commander";import{compress as a}from"./compress.js";import{OUTPUT_FORMATS as p}from"./compressImage.js";import{watermark as s}from"./watermark.js";import{WATERMARK_POSITIONS as c}from"./watermarkImage.js";function m(n){const o=Number.parseInt(n,10);if(Number.isNaN(o)||o<1||o>100)throw new r("\u5FC5\u987B\u662F 1 - 100 \u7684\u6574\u6570");return o}function u(n){const o=Number.parseInt(n,10);if(Number.isNaN(o)||o<=0)throw new r("\u5FC5\u987B\u662F\u6B63\u6574\u6570");return o}function e(n){const o=n.trim();if(o===""||o==="."||o===".."||o.includes("/")||o.includes("\\"))throw new r("\u5FC5\u987B\u662F\u5355\u7EAF\u7684\u76EE\u5F55\u540D\uFF08\u4E0D\u80FD\u4E3A\u7A7A\uFF0C\u4E0D\u80FD\u5305\u542B / \\ . ..\uFF09");return o}function i(n){const o=Number.parseInt(n,10);if(Number.isNaN(o)||o<1||o>1024)throw new r("\u5FC5\u987B\u662F 1 - 1024 \u7684\u6574\u6570");return o}function f(n){const o=n.trim();if(o==="")throw new r("\u8F93\u51FA\u8DEF\u5F84\u4E0D\u80FD\u4E3A\u7A7A");return o}function N(n){const o=Number(n);if(Number.isNaN(o)||o<.1||o>1)throw new r("\u5FC5\u987B\u662F 0.1 - 1 \u4E4B\u95F4\u7684\u5C0F\u6570");return o}function l(n){const o=Number.parseInt(n,10);if(Number.isNaN(o)||o<1||o>100)throw new r("\u5FC5\u987B\u662F 1 - 100 \u7684\u6574\u6570");return o}function w(n){const o=Number.parseInt(n,10);if(Number.isNaN(o)||o<-89||o>89)throw new r("\u5FC5\u987B\u662F -89 - 89 \u7684\u6574\u6570\uFF080 = \u4E0D\u503E\u659C\uFF09");return o}const d=new t("-f, --format <format>","\u8F6C\u6362\u8F93\u51FA\u683C\u5F0F\uFF08\u9ED8\u8BA4\u4FDD\u6301\u539F\u683C\u5F0F\uFF09").choices([...p]);export function myCompress(n){n.command("compress [target]").alias("c").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\u8C03\u8272\u677F\u989C\u8272\u6570\uFF09",m,80).addOption(d).option("-w, --max-width <px>","\u7B49\u6BD4\u7F29\u653E\u7684\u6700\u5927\u5BBD\u5EA6\uFF0C\u53EA\u7F29\u5C0F\u4E0D\u653E\u5927",u).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",e).option("-c, --concurrency <n>","\u540C\u65F6\u5904\u7406\u591A\u5C11\u5F20\u56FE\u7247",i,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",f).option("-s, --smart","\u6BCF\u5F20\u56FE\u5206\u522B\u6309\u591A\u4E2A\u5019\u9009\u683C\u5F0F\u7F16\u7801",!1).action(a)}const b=new t("-p, --position <pos>","\u6C34\u5370\u4F4D\u7F6E\uFF08\u4E5D\u5BAB\u683C\u7F29\u5199\uFF09").choices([...c]).default("br");export function myWatermark(n){n.command("watermark [target]").alias("wm").description("\u6279\u91CF\u7ED9\u56FE\u7247\u52A0\u6C34\u5370\uFF1A\u56FE\u7247 logo \u6216\u6587\u5B57\uFF0C\u652F\u6301\u4E5D\u5BAB\u683C\u5B9A\u4F4D\u4E0E\u5E73\u94FA\u9632\u76D7\u56FE").option("-m, --mark <file>","\u6C34\u5370\u56FE\u7247\u8DEF\u5F84\uFF08\u4E0E -t \u4E92\u65A5\uFF09").option("-t, --text <string>","\u6C34\u5370\u6587\u5B57\uFF08\u4E0E -m \u4E92\u65A5\uFF09").addOption(b).option("--opacity <0.1-1>","\u4E0D\u900F\u660E\u5EA6\uFF0C\u9ED8\u8BA4 0.5",N,.5).option("-s, --scale <1-100>","\u6C34\u5370\u5927\u5C0F = \u4E3B\u56FE\u5BBD\u7684\u767E\u5206\u6BD4\uFF08\u9ED8\u8BA4\uFF1A\u56FE\u7247 15\uFF0C\u6587\u5B57 5\uFF09",l).option("--tile","\u5E73\u94FA\u6574\u5F20\u56FE\uFF08\u9632\u76D7\u56FE\uFF09",!1).option("-r, --recursive","\u9012\u5F52\u5904\u7406\u5B50\u76EE\u5F55",!1).option("--dry","\u9884\u89C8\u6A21\u5F0F\uFF1A\u771F\u5B9E\u5408\u6210\uFF0C\u4F46\u4E0D\u5199\u4EFB\u4F55\u6587\u4EF6",!1).option("-y, --yes","\u8DF3\u8FC7\u5168\u90E8\u95EE\u7B54\u3001\u76F4\u63A5\u7528\u9ED8\u8BA4\u503C",!1).option("--backup-dir <name>","\u5907\u4EFD\u76EE\u5F55\u540D\uFF08\u9ED8\u8BA4 .backup\uFF09",e).option("-c, --concurrency <n>","\u540C\u65F6\u5904\u7406\u591A\u5C11\u5F20\u56FE\u7247",i,4).option("-a, --angle <deg>","\u5E73\u94FA\u6C34\u5370\u7684\u503E\u659C\u89D2\u5EA6\uFF08\u5EA6\uFF0C0 = \u4E0D\u503E\u659C\uFF09",w).action(s)}
@@ -1 +1 @@
1
- import v from"sharp";import p from"path";import{randomUUID as T}from"crypto";import{readFile as L,rename as k,unlink as _,writeFile as B,mkdir as E}from"fs/promises";import{backupFile as U,replaceExt as O}from"#lib/utils/fs";export const OUTPUT_FORMATS=["jpeg","png","webp","avif","tiff","gif"];const W=["heif","svg"];export const EXT_BY_FORMAT={jpeg:".jpg",png:".png",webp:".webp",avif:".avif",tiff:".tiff",gif:".gif"},FALLBACK_OUTPUT={heif:"jpeg",svg:"png"},EXT_TO_INPUT_FORMAT={".jpg":"jpeg",".jpeg":"jpeg",".png":"png",".webp":"webp",".avif":"avif",".tiff":"tiff",".gif":"gif",".heic":"heif",".heif":"heif",".svg":"svg"};export function isOutputFormat(t){return OUTPUT_FORMATS.includes(t)}export function baseOutputFormat(t){return isOutputFormat(t)?t:FALLBACK_OUTPUT[t]}function A(t){const e=Math.round((100-t)/100*9);return t<=60?{compressionLevel:e,palette:!0}:{compressionLevel:e}}export function applyOutputFormat(t,e,o){switch(e){case"jpeg":return t.jpeg({quality:o,mozjpeg:!0});case"png":return t.png(A(o));case"webp":return t.webp({quality:o});case"avif":return t.avif({quality:o});case"tiff":return t.tiff({quality:o});case"gif":return t.gif()}}export async function compressOne(t,e,o,l){const r={file:t,output:t,format:"jpeg",beforeBytes:0,afterBytes:0,status:"failed"};try{const n=await L(t);r.beforeBytes=n.byteLength;const h=p.extname(t).toLowerCase(),m=EXT_TO_INPUT_FORMAT[h];if(m===void 0)throw new Error(`\u4E0D\u652F\u6301\u7684\u56FE\u7247\u6269\u5C55\u540D\uFF1A${h===""?"\u65E0\u6269\u5C55\u540D":h}`);const w=await v(n).metadata(),x=a=>{let s=v(n).rotate();return e.maxWidth!==void 0&&w.width!==void 0&&w.width>e.maxWidth&&(s=s.resize({width:e.maxWidth,withoutEnlargement:!0})),a.format==="jpeg"&&w.hasAlpha&&s.flatten({background:"#ffffff"}),a.lossless?s.webp({lossless:!0}):applyOutputFormat(s,a.format,e.quality)},c=baseOutputFormat(m),F=e.smart?[{format:c},{format:"webp"},...c==="png"?[{format:"webp",lossless:!0}]:[]]:[{format:e.format??c}];let i;for(const a of F){const{data:s}=await x(a).toBuffer({resolveWithObject:!0});(i===void 0||s.byteLength<i.data.byteLength)&&(i={format:a.format,data:s})}if(i===void 0)throw new Error("\u6CA1\u6709\u53EF\u7528\u7684\u8F93\u51FA\u5019\u9009");const d=i.format,g=d===m,u=(e.smart||g)&&i.data.byteLength>=n.byteLength,j=p.relative(o,t),y=u||g?j:O(j,EXT_BY_FORMAT[d]),f=e.outDir!==void 0?p.join(e.outDir,y):u||g?t:O(t,EXT_BY_FORMAT[d]);if(r.output=f,r.format=d,r.afterBytes=i.data.byteLength,u&&(r.afterBytes=n.byteLength,r.format=c),u&&e.outDir===void 0)return r.status="skipped",r;if(f!==t&&l.has(f))throw new Error(`\u8F93\u51FA\u8DEF\u5F84\u51B2\u7A81\uFF1A${p.basename(f)} \u5DF2\u88AB\u672C\u6279\u5176\u4ED6\u6587\u4EF6\u5360\u7528`);if(l.add(f),e.dry)return r.status=u?"skipped":"done",r;g&&e.outDir===void 0&&await U(o,t,e.backupDir),e.outDir!==void 0&&await E(p.dirname(f),{recursive:!0});const b=`${f}${T()}.tmp`;try{await B(b,u?n:i.data),await k(b,f)}catch(a){throw await _(b).catch(()=>{}),a}return r.status=u?"skipped":"done",r}catch(n){return r.error=n instanceof Error?n.message:String(n),r}}
1
+ import v from"sharp";import c from"path";import{randomUUID as L}from"crypto";import{readFile as T,rename as k,unlink as _,writeFile as E,mkdir as A}from"fs/promises";import{backupFile as B,replaceExt as j}from"#lib/utils/fs";export const OUTPUT_FORMATS=["jpeg","png","webp","avif","tiff","gif"];const N=["heif","svg"];export const EXT_BY_FORMAT={jpeg:".jpg",png:".png",webp:".webp",avif:".avif",tiff:".tiff",gif:".gif"},FALLBACK_OUTPUT={heif:"jpeg",svg:"png"},EXT_TO_INPUT_FORMAT={".jpg":"jpeg",".jpeg":"jpeg",".png":"png",".webp":"webp",".avif":"avif",".tiff":"tiff",".gif":"gif",".heic":"heif",".heif":"heif",".svg":"svg"};const Q=[{minQuality:80,colours:256,dither:1},{minQuality:70,colours:256,dither:.5},{minQuality:60,colours:16,dither:1},{minQuality:50,colours:16,dither:.5},{minQuality:40,colours:16,dither:0},{minQuality:10,colours:4,dither:0},{minQuality:1,colours:2,dither:0}];export function isOutputFormat(t){return OUTPUT_FORMATS.includes(t)}export function baseOutputFormat(t){return isOutputFormat(t)?t:FALLBACK_OUTPUT[t]}function U(t){if(t>80)return{compressionLevel:9,adaptiveFiltering:!0};const e=Q.find(i=>t>=i.minQuality);return e===void 0?{compressionLevel:9}:{compressionLevel:9,palette:!0,colours:e.colours,dither:e.dither}}export function applyOutputFormat(t,e,i){switch(e){case"jpeg":return t.jpeg({quality:i,mozjpeg:!0});case"png":return t.png(U(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,b){const r={file:t,output:t,format:"jpeg",beforeBytes:0,afterBytes:0,status:"failed"};try{const n=await T(t);r.beforeBytes=n.byteLength;const m=c.extname(t).toLowerCase(),h=EXT_TO_INPUT_FORMAT[m];if(h===void 0)throw new Error(`\u4E0D\u652F\u6301\u7684\u56FE\u7247\u6269\u5C55\u540D\uFF1A${m===""?"\u65E0\u6269\u5C55\u540D":m}`);const l=await v(n).metadata(),F=a=>{let s=v(n).rotate();return e.maxWidth!==void 0&&l.width!==void 0&&l.width>e.maxWidth&&(s=s.resize({width:e.maxWidth,withoutEnlargement:!0})),a.format==="jpeg"&&l.hasAlpha&&s.flatten({background:"#ffffff"}),a.lossless?s.webp({lossless:!0}):applyOutputFormat(s,a.format,e.quality)},p=baseOutputFormat(h),O=e.smart?[{format:p},{format:"webp"},...p==="png"?[{format:"webp",lossless:!0}]:[]]:[{format:e.format??p}];let o;for(const a of O){const{data:s}=await F(a).toBuffer({resolveWithObject:!0});(o===void 0||s.byteLength<o.data.byteLength)&&(o={format:a.format,data:s})}if(o===void 0)throw new Error("\u6CA1\u6709\u53EF\u7528\u7684\u8F93\u51FA\u5019\u9009");const d=o.format,g=d===h,u=(e.smart||g)&&o.data.byteLength>=n.byteLength,y=c.relative(i,t),x=u||g?y:j(y,EXT_BY_FORMAT[d]),f=e.outDir!==void 0?c.join(e.outDir,x):u||g?t:j(t,EXT_BY_FORMAT[d]);if(r.output=f,r.format=d,r.afterBytes=o.data.byteLength,u&&(r.afterBytes=n.byteLength,r.format=p),u&&e.outDir===void 0)return r.status="skipped",r;if(f!==t&&b.has(f))throw new Error(`\u8F93\u51FA\u8DEF\u5F84\u51B2\u7A81\uFF1A${c.basename(f)} \u5DF2\u88AB\u672C\u6279\u5176\u4ED6\u6587\u4EF6\u5360\u7528`);if(b.add(f),e.dry)return r.status=u?"skipped":"done",r;g&&e.outDir===void 0&&await B(i,t,e.backupDir),e.outDir!==void 0&&await A(c.dirname(f),{recursive:!0});const w=`${f}${L()}.tmp`;try{await E(w,u?n:o.data),await k(w,f)}catch(a){throw await _(w).catch(()=>{}),a}return r.status=u?"skipped":"done",r}catch(n){return r.error=n instanceof Error?n.message:String(n),r}}
package/package.json CHANGED
@@ -1,10 +1,11 @@
1
1
  {
2
2
  "name": "cantarella",
3
- "version": "1.2.0",
3
+ "version": "1.3.1",
4
4
  "description": "批量压缩图片的命令行工具",
5
5
  "type": "module",
6
6
  "bin": {
7
- "cantarella": "dist/bin/cli.js"
7
+ "cantarella": "dist/bin/cli.js",
8
+ "ct": "dist/bin/cli.js"
8
9
  },
9
10
  "scripts": {
10
11
  "start": "node --conditions=development bin/cli.ts --help",