cantarella 1.0.2 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -2,9 +2,9 @@
2
2
 
3
3
  # 🖼️ cantarella
4
4
 
5
- **批量压缩图片的命令行工具**
5
+ **批量压缩与加水印的命令行工具**
6
6
 
7
- 传入一个文件夹或单张图片,默认压缩覆盖(覆盖前把原图备份到备份目录,默认 `.backup/`,可用 `--backup-dir` 自定义);也可用 `--out` 把结果写进独立目录树,原图一个字节都不动。
7
+ 两个子命令:`compress` 压缩(默认覆盖原文件,覆盖前自动备份到 `.backup/`,可用 `--backup-dir` 自定义;或用 `--out` 写进独立目录树,原图一个字节都不动);`watermark` 批量加水印(图片 logo 或文字,九宫格定位、平铺防盗图、可调倾斜角)。
8
8
 
9
9
  [![Node.js](https://img.shields.io/badge/node-%E2%89%A522.12-brightgreen)](https://nodejs.org/)
10
10
  [![License: ISC](https://img.shields.io/badge/License-ISC-blue.svg)](./LICENSE)
@@ -17,8 +17,10 @@
17
17
  ## ✨ 特性
18
18
 
19
19
  - **两种使用方式**:不传参数进入交互问答模式;传参数则为纯 CLI 模式,可写进脚本;`-y` 全默认模式让裸命令也能进脚本
20
+ - **`watermark` 批量加水印**:图片 logo(`-m`)或文字(`-t`)二选一;九宫格定位(`-p`)、不透明度(`--opacity`)、大小按主图宽百分比(`-s`);`--tile` 平铺防盗图,`-a` 斜向平铺;覆盖前同样自动备份,`--dry` 零写入预览;手机竖拍照片自动摆正(EXIF orientation)
20
21
  - **安全第一**:覆盖原文件前自动备份(目录名可用 `--backup-dir` 自定义,带撞名防护,绝不共用你已有的文件夹);重压缩不会变小的文件自动跳过
21
22
  - **`--out` 输出模式**:结果写进独立目录树(镜像源目录结构),不动原图、不写备份;压不动的文件原样拷贝进去,目录树永远完整;同名输出冲突(如 heic→jpg 撞已有 a.jpg)被拦截报错而不是静默覆盖
23
+ - **`-s` 智能模式**:每张图按候选格式真实编码比大小、哪个小留哪个(png 输入额外加赛无损 webp);所有候选都没赢过原图就跳过,绝不产出更大的文件
22
24
  - **`--dry` 预览**:真实编码计算压缩后大小,但不写任何文件
23
25
  - **`--report` 报告**:在目标目录生成 markdown 压缩报告
24
26
  - **格式转换**:jpeg / png / webp / avif / tiff / gif 互转,支持 heic(iPhone 照片)与 svg 输入
@@ -56,12 +58,21 @@ cantarella compress ./photos --report # 生成 markdown 压缩报告
56
58
  cantarella compress -y # 跳过全部问答,全默认直接压当前目录(脚本友好)
57
59
  cantarella compress ./photos --backup-dir bak # 备份到自定义目录 bak/ 而不是 .backup/
58
60
  cantarella compress ./photos -o dist # 输出到独立目录 dist/(镜像结构,不动原图)
61
+ cantarella compress ./photos -s # 智能模式:每张图自动选体积最小的格式
62
+ cantarella watermark # 交互模式:选水印类型逐项问答
63
+ cantarella watermark ./photos -t "© LeoZhao" # 文字水印,默认右下角
64
+ cantarella watermark ./photos -m logo.png # 图片水印(透明底 png 最佳)
65
+ cantarella watermark ./photos -t "内部资料" --tile -a 30 -s 8 # 斜向平铺防盗图
66
+ cantarella watermark ./photos -m logo.png -p c --opacity 0.8 -s 20 # 居中、80% 不透明度、宽 20%
67
+ cantarella watermark ./photos -t "x" --dry # 预览:真实合成但不写任何文件
59
68
  ```
60
69
 
61
70
  交互模式中直接回车即采用默认值;ESC / Ctrl+C 随时取消。
62
71
 
63
72
  ## ⚙️ 选项
64
73
 
74
+ ### compress
75
+
65
76
  | 选项 | 说明 | 默认值 |
66
77
  | ------------------------ | ---------------------------------------------------------------------------------------------------------- | --------------------- |
67
78
  | `-q, --quality <number>` | 压缩质量 1-100(png 映射为压缩等级) | `80` |
@@ -74,6 +85,24 @@ cantarella compress ./photos -o dist # 输出到独立目录 dist/(
74
85
  | `--backup-dir <name>` | 备份目录名(须为单纯目录名;非空且非本工具创建的目录会被拒绝使用) | `.backup` |
75
86
  | `-c, --concurrency <n>` | 同时处理几张图,1-1024(调大超过 4 时建议同时设 `UV_THREADPOOL_SIZE` 环境变量同值) | `4` |
76
87
  | `-o, --out <dir>` | 输出到独立目录(镜像源目录结构,不动原图、不写备份;压不动的文件原样拷贝保证树完整;不能指向目标目录本身) | 无(原地覆盖 + 备份) |
88
+ | `-s, --smart` | 智能模式:每张图在原格式与 webp 间实测选最小(png 额外加赛无损 webp);与 `-f` 互斥 | 关闭 |
89
+
90
+ ### watermark
91
+
92
+ | 选项 | 说明 | 默认值 |
93
+ | ------------------------ | ----------------------------------------------------------------------------------------------------------- | --------------------- |
94
+ | `-m, --mark <file>` | 水印图片路径(透明底 png 最佳;与 `-t` 互斥) | 无 |
95
+ | `-t, --text <string>` | 水印文字(与 `-m` 互斥) | 无 |
96
+ | `-p, --position <pos>` | 水印位置九宫格:`tl` `t` `tr` `l` `c` `r` `bl` `b` `br`(平铺模式下无效) | `br` |
97
+ | `--opacity <0.1-1>` | 不透明度 | `0.5` |
98
+ | `-s, --scale <1-100>` | 水印大小 = 主图宽的百分比(文字即字号基准) | 图片 `15` / 文字 `5` |
99
+ | `--tile` | 平铺整张图(防盗图) | 关闭 |
100
+ | `-a, --angle <deg>` | 平铺倾斜角度,`-89` ~ `89`(0 = 不倾斜;仅在 `--tile` 下生效) | `0` |
101
+ | `-r, --recursive` | 递归子目录(与 compress 同义) | 关闭 |
102
+ | `--dry` | 预览模式:真实合成但不写任何文件(与 compress 同义) | 关闭 |
103
+ | `-y, --yes` | 跳过全部问答、直接用默认值(与 compress 同义) | 关闭 |
104
+ | `--backup-dir <name>` | 备份目录名(与 compress 同义) | `.backup` |
105
+ | `-c, --concurrency <n>` | 同时处理几张图(与 compress 同义) | `4` |
77
106
 
78
107
  ## 📋 支持格式
79
108
 
@@ -117,7 +146,21 @@ pnpm build # 编译并压缩到 dist/(发布、npm link 前需要)
117
146
 
118
147
  ## 📜 版本记录
119
148
 
120
- ### 🚀 1.0.2 · 2026-09-06
149
+ ### 🚀 1.2.0 · 2026-09-15
150
+
151
+ - ✨ **新增** `watermark` 子命令:批量给图片加水印——图片 logo(`-m`)或文字(`-t`)二选一;九宫格定位(`-p`)、不透明度(`--opacity`)、大小按主图宽百分比(`-s`,图片默认 15 / 文字默认 5)
152
+ - ✨ **新增** `--tile` 平铺防盗图与 `-a, --angle` 斜向平铺:sharp 的 tile 只会轴对齐复制,采用"转贴纸不转网格"(先旋转水印本体再平铺);格子比小图还大时自动等比缩入,不再整张失败
153
+ - 🛡️ 与 compress 同款安全机制:覆盖前自动备份、`--dry` 零写入预览、输出路径冲突(如 svg→png 撞已有 a.png)拦截报错、heic/svg 自动转出、交互问答 ESC 随时取消
154
+ - 🛡️ 手机竖拍照片(EXIF orientation ≥ 5)自动摆正后再贴水印,不会出现"躺平"或水印错位
155
+ - 🐛 **修复**(compress):`-s` 与 `-f` 同时给出时报错信息不显示的问题
156
+
157
+ ### 🚀 1.1.0 · 2026-09-08
158
+
159
+ - ✨ **新增** `-s, --smart` 智能模式:每张图按候选格式各真实编码一次——原格式 + webp,png 输入额外加赛无损 webp(无损赛道对截图/图形类常比 png 再小 10-25%),哪个小留哪个;所有候选都没赢过原图时跳过(负优化保护),跳过的文件保持原扩展名(覆盖模式原图不动、`--out` 模式原样拷贝)
160
+ - 🛡️ 与 `-f` 互斥(同时给出直接报错退出);交互模式下自动跳过"输出格式"问答
161
+ - 💡 smart 每张图编码 2-3 次,批量跑比平时慢是正常现象;`--dry` 预览显示每张图的格式选择(如 `(.png -> .webp)`),报告"输出格式"列可见全部决策
162
+
163
+ ### 🚀 1.0.2 · 2026-09-07
121
164
 
122
165
  - ✨ **新增** `-c, --concurrency <n>` 并发选项:CLI 与交互问答双通道,有效范围 1-1024(与 libuv 线程池上限对齐),并发数写入压缩报告的参数行
123
166
  - 💡 调大并发(>4)时建议同时设置环境变量 `UV_THREADPOOL_SIZE` 同值——sharp 的编解码跑在 libuv 线程池(默认 4 线程),只调 `-c` 不调线程池不会更快
package/dist/bin/cli.js CHANGED
@@ -1,2 +1,2 @@
1
1
  #! /usr/bin/env node
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);
2
+ import{createRequire as o}from"node:module";import{program as r}from"commander";import{myCompress as m,myWatermark as s}from"#lib/core/command";const{version:a}=o(import.meta.url)("#pkg");r.name("cantarella"),r.version(a),m(r),s(r),r.parseAsync(process.argv).catch(e=>{console.log(e),process.exitCode=1});
@@ -1 +1 @@
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
+ 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 +1 @@
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
+ import*as t from"@clack/prompts";import j from"chalk";import A from"p-limit";import f from"node:path";import{mkdir as L}from"node:fs/promises";import{backupDirState as U,collectImages as M,formatBytes as g,formatPercent as S,markBackupDir as _,resolveRoot as z}from"#lib/utils/fs";import{accent as G}from"#lib/utils/theme";import{compressOne as H,OUTPUT_FORMATS as J}from"./compressImage.js";import{buildReport as K,writeReport as Q}from"./report.js";import w from"#config";export const compress=async($,a,T)=>{const c=t.spinner();if(t.intro(G("Cantarella \u56FE\u7247\u538B\u7F29\u5DE5\u5177")),a.smart&&a.format!==void 0){t.log.error("smart \u6A21\u5F0F\u4E0B\u4E0D\u80FD\u6307\u5B9A\u8F93\u51FA\u6587\u4EF6\u683C\u5F0F"),process.exitCode=1;return}const p=$===void 0?process.cwd():f.resolve($.trim());let y=a.recursive??!1,x=a.report??!1;const m=a.backupDir??w.compress.backupDir;if($===void 0&&!a.yes){const s=r=>T.getOptionValueSource(r)==="cli",i=await t.group({...s("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"}})},...s("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"}})},...s("format")||a.smart?{}:{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"},...J.map(r=>({value:r,label:r}))]})},...s("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"}})},...s("recursive")?{}:{recursive:()=>t.confirm({message:"\u9012\u5F52\u5904\u7406\u5B50\u76EE\u5F55\uFF1F",initialValue:!1})},...s("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)}});i.concurrency!==void 0&&(a.concurrency=Number(i.concurrency)),i.quality!==void 0&&(a.quality=Number(i.quality)),i.format!==void 0&&(a.format=i.format),i.maxWidth!==void 0&&Number(i.maxWidth)>0&&(a.maxWidth=Number(i.maxWidth)),i.recursive!==void 0&&(y=i.recursive),i.report!==void 0&&(x=i.report)}const V=a.quality??80,k=a.concurrency??4,F=a.format===void 0||a.format===""?void 0:a.format,I=a.maxWidth,d=a.dry??!1,O=a.smart??!1,u=a.out!==void 0?f.resolve(a.out):void 0;try{c.start("\u6B63\u5728\u626B\u63CF\u6587\u4EF6...");let s="",i=!1;try{const e=await z(p);s=e.root,i=e.isFile}catch{c.error(`\u8DEF\u5F84\u4E0D\u5B58\u5728\u6216\u65E0\u6CD5\u8BBF\u95EE\uFF1A${p}`),process.exitCode=1;return}if(u!==void 0&&u===s){c.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(i){const e=f.extname(p).toLowerCase();if(!w.compress.extensions.includes(e)){c.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 M(s,y,m);if(r.length===0){c.stop(),t.outro("\u6CA1\u6709\u627E\u5230\u53EF\u5904\u7406\u7684\u56FE\u7247");return}if(u===void 0){const e=await U(s,m);if(e==="foreign"&&m!==w.compress.backupDir){c.error(`\u5907\u4EFD\u76EE\u5F55 ${m} \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"&&!d&&await _(s,m)}const n={quality:V,format:F,maxWidth:I,dry:d,backupDir:m,outDir:u,smart:O},R=A(k),E=new Set(r);let b=0;c.message(`${d?"\u9884\u89C8":"\u538B\u7F29"} ${b}/${r.length}`);const l=await Promise.all(r.map(e=>R(async()=>{const o=await H(e,n,s,E);return b+=1,c.message(`${d?"\u9884\u89C8":"\u538B\u7F29"} ${b}/${r.length} ${f.basename(e)}`),o}))),v=l.filter(e=>e.status==="skipped"),h=l.filter(e=>e.status==="failed"),C=l.reduce((e,o)=>e+o.beforeBytes,0),N=l.reduce((e,o)=>e+(o.status==="failed"?o.beforeBytes:o.afterBytes),0),B=[`${d?"\u9884\u89C8":"\u538B\u7F29"}\u5B8C\u6210\uFF1A${l.length} \u4E2A\u6587\u4EF6`,`${g(C)} \u2192 ${g(N)}`,`\u8282\u7701 ${S(C,N)}`];if(v.length>0&&B.push(`\u8DF3\u8FC7 ${v.length}`),c.stop(),t.log.success(B.join(",")),h.length>0){t.log.error(`\u5931\u8D25 ${h.length} \u4E2A\uFF1A`);for(const e of h)t.log.error(`${f.relative(s,e.file)} \u2014\u2014 ${e.error??"\u672A\u89E3\u6790\u51FA\u53D1\u751F\u4E86\u4EC0\u4E48"}`);process.exitCode=1}if(d){t.log.message(j.bold("\u9884\u89C8\u660E\u7EC6\uFF08\u672A\u5199\u5165\u4EFB\u4F55\u6587\u4EF6\uFF09\uFF1A"));for(const e of l){const o=f.relative(s,e.file),D=u!==void 0&&e.status!=="failed"?` -> ${f.relative(u,e.output)}`:"";if(e.status==="failed")t.log.error(`${o} \u2014\u2014 ${e.error}`);else if(e.status==="skipped")t.log.warn(u===void 0?`${o} \u4F1A\u8DF3\u8FC7\uFF08\u91CD\u538B\u7F29\u4E0D\u4F1A\u53D8\u5C0F\uFF09`:`${o}${D} \u4E0D\u538B\u7F29\u3001\u539F\u6837\u590D\u5236\uFF08\u91CD\u538B\u7F29\u4E0D\u4F1A\u53D8\u5C0F\uFF09`);else{const W=f.extname(e.file),q=f.extname(e.output),P=W!==q?`\uFF08${W} -> ${q}\uFF09`:"";t.log.success(`${o}${D} ${g(e.beforeBytes)} => ${g(e.afterBytes)}\uFF08\u8282\u7701 ${S(e.beforeBytes,e.afterBytes)}\uFF09${P}`)}}x&&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(x){const e={target:p,root:s,recursive:y,concurrency:k,params:n,startedAt:new Date};u!==void 0&&await L(u,{recursive:!0});const o=await Q(u??s,K(e,l));t.log.info(`\u62A5\u544A\u5DF2\u751F\u6210\uFF1A${o}`),t.outro("\u5B8C\u6210")}}catch(s){c.stop(),t.log.error(s instanceof Error?s.message:String(s)),process.exitCode=1}};
@@ -1 +1 @@
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}}
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}}
@@ -0,0 +1 @@
1
+ import*as r from"@clack/prompts";import O from"p-limit";import m from"path";import{readFile as T}from"fs/promises";import{backupDirState as A,collectImages as L,formatBytes as y,markBackupDir as F,resolveRoot as P}from"#lib/utils/fs";import{accent as V}from"#lib/utils/theme";import C from"#config";import{POSITION_LABEL as E,WATERMARK_POSITIONS as R,describeWatermark as W,watermarkOne as _}from"./watermarkImage.js";const j=(f,e)=>{if(f<=0)return"-";const p=(e/f-1)*100;return`${p>=0?"+":""}${p.toFixed(1)}%`};export const watermark=async(f,e,p)=>{const o=r.spinner();if(r.intro(V("Cantarella \u56FE\u7247\u6C34\u5370\u5DE5\u5177")),e.mark!==void 0&&e.mark.trim()===""&&(e.mark=void 0),e.text!==void 0&&e.text.trim()===""&&(e.text=void 0),e.mark!==void 0&&e.text!==void 0){r.log.error("-m \u548C -t \u53EA\u80FD\u4E8C\u9009\u4E00"),process.exitCode=1;return}if(!(e.mark!==void 0||e.text!==void 0)&&(f!==void 0||e.yes)){r.log.error("\u8BF7\u7528 -m \u6307\u5B9A\u6C34\u5370\u56FE\u7247\uFF0C\u6216 -t \u6307\u5B9A\u6C34\u5370\u6587\u5B57"),process.exitCode=1;return}const $=f===void 0?process.cwd():m.resolve(f.trim());let S=e.recursive??!1;const d=e.backupDir??C.compress.backupDir,u=e.dry??!1;if(f===void 0&&!e.yes){const a=s=>p.getOptionValueSource(s)==="cli",b=()=>{r.cancel("\u7528\u6237\u53D6\u6D88\u64CD\u4F5C"),process.exit(0)},l=a("mark")?"mark":a("text")?"text":await r.select({message:"\u6C34\u5370\u7C7B\u578B",options:[{value:"mark",label:"\u56FE\u7247\u6C34\u5370",hint:"logo \u89D2\u6807\uFF0C\u900F\u660E\u5E95\u6700\u4F73"},{value:"text",label:"\u6587\u5B57\u6C34\u5370",hint:"\xA9 / \u5185\u90E8\u8D44\u6599"}]});r.isCancel(l)&&(r.cancel("\u7528\u6237\u53D6\u6D88\u64CD\u4F5C"),process.exit(0));const g=l,x=a("tile")?e.tile??!1:await r.confirm({message:"\u5E73\u94FA\u9632\u76D7\u56FE\uFF1F",initialValue:!1});r.isCancel(x)&&(r.cancel("\u7528\u6237\u53D6\u6D88\u64CD\u4F5C"),process.exit(0)),e.tile=x;const i=await r.group({...g==="mark"&&!a("mark")?{mark:()=>r.text({message:"\u6C34\u5370\u56FE\u7247\u8DEF\u5F84",placeholder:"logo.png",validate:s=>(s??"").trim()===""?"\u8BF7\u8F93\u5165\u8DEF\u5F84":void 0})}:{},...g==="text"&&!a("text")?{text:()=>r.text({message:"\u6C34\u5370\u6587\u5B57",placeholder:"\xA9 LeoZhao",validate:s=>(s??"").trim()===""?"\u8BF7\u8F93\u5165\u5185\u5BB9":void 0})}:{},...x||a("position")?{}:{position:()=>r.select({message:"\u6C34\u5370\u4F4D\u7F6E",options:R.map(s=>({value:s,label:E[s]}))})},...x&&!a("angle")?{angle:()=>r.text({message:"\u5E73\u94FA\u503E\u659C\u89D2\u5EA6\uFF08\u5EA6\uFF09",defaultValue:"30",validate:s=>{if(s===void 0||s.trim()==="")return;const n=Number(s);if(!Number.isInteger(n)||n<-89||n>89)return"\u8BF7\u8F93\u5165 -89 - 89 \u7684\u6574\u6570\uFF080 = \u4E0D\u503E\u659C\uFF09"}})}:{},...a("scale")?{}:{scale:()=>r.text({message:"\u6C34\u5370\u5927\u5C0F\uFF08\u4E3B\u56FE\u5BBD\u7684\u767E\u5206\u6BD4\uFF09",defaultValue:g==="mark"?"15":"5",validate:s=>{if(s===void 0||s.trim()==="")return;const n=Number(s);if(!Number.isInteger(n)||n<1||n>100)return"\u8BF7\u8F93\u5165 1 - 100 \u7684\u6574\u6570"}})}},{onCancel:b});i.mark!==void 0&&(e.mark=i.mark),i.text!==void 0&&(e.text=i.text),i.angle!==void 0&&(e.angle=Number(i.angle)),i.position!==void 0&&(e.position=i.position),i.scale!==void 0&&(e.scale=Number(i.scale))}let h,B;if(e.mark!==void 0){const a=m.resolve(e.mark.trim());try{h=await T(a)}catch{r.log.error(`\u65E0\u6CD5\u8BFB\u53D6\u6C34\u5370\u56FE\u7247\uFF1A${a}`),process.exitCode=1;return}B=m.basename(a)}const v={markBuffer:h,markName:B,text:e.text,position:e.position??"br",opacity:e.opacity??.5,scale:e.scale??(h!==void 0?15:5),tile:e.tile??!1,angle:e.angle??0},N=e.concurrency??4;try{o.start("\u6B63\u5728\u626B\u63CF\u6587\u4EF6......");let a="",b=!1;try{const t=await P($);a=t.root,b=t.isFile}catch{o.error(`\u8DEF\u5F84\u4E0D\u5B58\u5728\u6216\u65E0\u6CD5\u8BBF\u95EE\uFF1A${$}`),process.exitCode=1;return}let l;if(b){const t=m.extname($).toLowerCase();if(!C.compress.extensions.includes(t)){o.error(`\u4E0D\u652F\u6301\u7684\u56FE\u7247\u7C7B\u578B\uFF1A${t===""?"\uFF08\u65E0\u6269\u5C55\u540D\uFF09":t}`),process.exitCode=1;return}l=[$]}else l=await L(a,S,d);if(l.length===0){o.stop(),r.outro("\u6CA1\u6709\u627E\u5230\u53EF\u5904\u7406\u7684\u56FE\u7247");return}const g=await A(a,d);if(g==="foreign"&&d!==C.compress.backupDir){o.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}g!=="ours"&&!u&&await F(a,d);const x={watermark:v,dry:u,backupDir:d},i=O(N),s=new Set(l);let n=0;o.message(`${u?"\u9884\u89C8":"\u6C34\u5370"} ${n}/${l.length}`);const k=await Promise.all(l.map(t=>i(async()=>{const c=await _(t,x,a,s);return n+=1,o.message(`${u?"\u9884\u89C8":"\u6C34\u5370"} ${n}/${l.length} ${m.basename(t)}`),c}))),w=k.filter(t=>t.status==="failed"),D=k.reduce((t,c)=>t+c.beforeBytes,0),I=k.reduce((t,c)=>t+(c.status==="failed"?c.beforeBytes:c.afterBytes),0);if(o.stop(),r.log.success([`${u?"\u9884\u89C8":"\u6C34\u5370"}\u5B8C\u6210\uFF1A${k.length} \u4E2A\u6587\u4EF6`,W(v),`${y(D)} \u2192 ${y(I)}`].join("\uFF0C")),w.length>0){r.log.error(`\u5931\u8D25 ${w.length} \u4E2A\uFF1A`);for(const t of w)r.log.error(`${m.relative(a,t.file)} \u2014\u2014 ${t.error??"\u672A\u89E3\u6790\u51FA\u53D1\u751F\u4E86\u4EC0\u4E48"}`);process.exitCode=1}if(u){r.log.message("\u9884\u89C8\u660E\u7EC6\uFF08\u672A\u5199\u5165\u4EFB\u4F55\u6587\u4EF6\uFF09\uFF1A");for(const t of k){const c=m.relative(a,t.file);t.status==="failed"?r.log.error(`${c} \u2014\u2014 ${t.error}`):r.log.success(`${c} ${y(t.beforeBytes)} => ${y(t.afterBytes)}\uFF08${j(t.beforeBytes,t.afterBytes)}\uFF09`)}r.outro("\u9884\u89C8\u5B8C\u6210");return}r.outro("\u5B8C\u6210")}catch(a){o.stop(),r.log.error(a instanceof Error?a.message:String(a)),process.exitCode=1}};
@@ -0,0 +1 @@
1
+ import o from"sharp";import x from"path";import{randomUUID as y}from"crypto";import{readFile as B,rename as $,unlink as O,writeFile as T}from"fs/promises";import{backupFile as I,replaceExt as A}from"#lib/utils/fs";import{EXT_BY_FORMAT as E,EXT_TO_INPUT_FORMAT as _,applyOutputFormat as F,baseOutputFormat as v}from"./compressImage.js";export const WATERMARK_POSITIONS=["tl","t","tr","l","c","r","bl","b","br"],GRAVITY_BY_POSITION={tl:"northwest",t:"north",tr:"northeast",l:"west",c:"centre",r:"east",bl:"southwest",b:"south",br:"southeast"},POSITION_LABEL={tl:"\u5DE6\u4E0A",t:"\u4E0A\u4E2D",tr:"\u53F3\u4E0A",l:"\u5DE6\u4E2D",c:"\u6B63\u4E2D",r:"\u53F3\u4E2D",bl:"\u5DE6\u4E0B",b:"\u4E0B\u4E2D",br:"\u53F3\u4E0B"},WATERMARK_QUALITY=90;const R=t=>t.replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;"),L=(t,a)=>Math.max(1,Math.min(Math.round(t*a/100),Math.round(t*.9)));async function N(t,a,i){return o({text:{text:`<span foreground="white" alpha="${Math.round(a*100)}%">${R(t)}</span>`,font:`Microsoft YaHei ${i}`,rgba:!0}}).png().toBuffer()}async function P(t,a,i){const c=await o(t).resize({width:i}).png().toBuffer(),e=await o(c).metadata(),r=e.width??i,s=e.height??1,h=Buffer.from(`<svg xmlns="http://www.w3.org/2000/svg" width="${r}" height="${s}"><rect width="100%" height="100%" fill="rgba(255,255,255,${a})"/></svg>`);return o(c).composite([{input:h,blend:"dest-in"}]).png().toBuffer()}export async function buildWatermarkOverlay(t,a,i){const c=L(a,t.scale),e=t.markBuffer!==void 0?await P(t.markBuffer,t.opacity,c):await N(t.text??"",t.opacity,c);if(!t.tile)return{input:e,gravity:GRAVITY_BY_POSITION[t.position]};let r=e;t.angle!==0&&(r=await o(r).rotate(t.angle,{background:{r:0,g:0,b:0,alpha:0}}).png().toBuffer());const s=await o(r).metadata(),h=Math.round(Math.max(s.width??1,s.height??1)*.5),n=await o(r).extend({top:0,left:0,right:h,bottom:h,background:{r:0,g:0,b:0,alpha:0}}).png().toBuffer(),d=await o(n).metadata(),w=d.width??1,l=d.height??1,m=Math.min(1,a/w,i/l);return{input:m<1?await o(n).resize({width:Math.max(1,Math.round(w*m)),height:Math.max(1,Math.round(l*m))}).png().toBuffer():n,tile:!0}}export function describeWatermark(t){const a=t.markBuffer!==void 0?`\u56FE\u7247 ${t.markName??"\u672A\u547D\u540D"}`:`\u6587\u5B57"${t.text??""}"`,i=t.tile?`\u5E73\u94FA${t.angle!==0?` ${t.angle}\xB0`:""}`:POSITION_LABEL[t.position];return`${a} (${i})`}export async function watermarkOne(t,a,i,c){const e={file:t,output:t,format:"jpeg",beforeBytes:0,afterBytes:0,status:"failed"};try{const r=await B(t);e.beforeBytes=r.byteLength;const s=x.extname(t).toLowerCase(),h=_[s];if(h===void 0)throw new Error(`\u4E0D\u652F\u6301\u7684\u56FE\u7247\u6269\u5C55\u540D\uFF1A${s===""?"\u65E0\u6269\u5C55\u540D":s}`);const n=await o(r).metadata();if(n.width===void 0||n.height===void 0)throw new Error("\u65E0\u6CD5\u786E\u5B9A\u56FE\u7247\u5C3A\u5BF8");const d=(n.orientation??1)>=5,w=d?n.height:n.width,l=d?n.width:n.height,m=await buildWatermarkOverlay(a.watermark,w,l),f=v(h);let p=o(r).rotate();f==="jpeg"&&n.hasAlpha&&(p=p.flatten({background:"#ffffff"})),p=F(p.composite([m]),f,WATERMARK_QUALITY);const{data:b}=await p.toBuffer({resolveWithObject:!0}),k=f===h,u=k?t:A(t,E[f]);if(e.output=u,e.format=f,e.afterBytes=b.byteLength,u!==t&&c.has(u))throw new Error(`\u8F93\u51FA\u8DEF\u5F84\u51B2\u7A81\uFF1A${x.basename(u)} \u5DF2\u88AB\u672C\u6279\u5176\u4ED6\u6587\u4EF6\u5360\u7528`);if(c.add(u),a.dry)return e.status="done",e;k&&await I(i,t,a.backupDir);const g=`${u}${y()}.tmp`;try{await T(g,b),await $(g,u)}catch(M){throw await O(g).catch(()=>{}),M}return e.status="done",e}catch(r){return e.error=r instanceof Error?r.message:String(r),e}}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cantarella",
3
- "version": "1.0.2",
3
+ "version": "1.2.0",
4
4
  "description": "批量压缩图片的命令行工具",
5
5
  "type": "module",
6
6
  "bin": {