@duoyun/md-img-pull 1.0.0 → 1.1.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 +85 -85
- package/dist/main.cjs +97 -83
- package/package.json +51 -51
package/README.md
CHANGED
|
@@ -1,85 +1,85 @@
|
|
|
1
|
-
# md-img-pull
|
|
2
|
-
|
|
3
|
-
一个用于将 Markdown 中的网络图片下载到本地、统一重写引用路径并对超大图片进行压缩的命令行工具。适合将笔记、博客中的媒体资源“本地化”,让内容更可移植、更易备份。
|
|
4
|
-
|
|
5
|
-
## 功能概述
|
|
6
|
-
|
|
7
|
-
- 递归扫描文件夹或处理单个 `.md` 文件
|
|
8
|
-
- 仅处理以 `http` 开头的图片链接,已本地化的相对/本地路径会跳过
|
|
9
|
-
- 将图片保存到目标 Markdown 同级的 `assets/` 目录,文件名为内容哈希
|
|
10
|
-
- 对超过阈值的图片自动压缩,必要时统一转为 WebP,尽量保留动画
|
|
11
|
-
- 使用并发控制与进度动画,清晰显示下载/压缩过程
|
|
12
|
-
- 输出详细的处理日志(成功/失败、尺寸变化)到目标目录
|
|
13
|
-
|
|
14
|
-
## 安装
|
|
15
|
-
|
|
16
|
-
- 全局安装(如果已发布到 npm):
|
|
17
|
-
- `npm i -g @duoyun/md-img-pull` 或 `pnpm add -g @duoyun/md-img-pull`
|
|
18
|
-
- 命令:`md-img-p`
|
|
19
|
-
- 本地构建运行(当前仓库):
|
|
20
|
-
- 安装依赖:`pnpm i`
|
|
21
|
-
- 构建:`pnpm build`
|
|
22
|
-
- 运行:`node dist/main.cjs`
|
|
23
|
-
|
|
24
|
-
环境要求:推荐 Node.js ≥ 18。依赖 `sharp`,在常见平台会自动下载所需的 libvips。
|
|
25
|
-
|
|
26
|
-
## 使用说明
|
|
27
|
-
|
|
28
|
-
- 运行后按提示输入源路径,可以是:
|
|
29
|
-
- 某个 Markdown 文件路径,例如 `C:\Notes\foo.md`
|
|
30
|
-
- 某个目录路径,例如 `C:\Notes`
|
|
31
|
-
- 输出位置:
|
|
32
|
-
- 单文件模式:在同目录生成 `foo_localized/`,并包含 `assets/`
|
|
33
|
-
- 目录模式:在同级生成 `Notes_localized/`,保持原有层级结构与 `assets/`
|
|
34
|
-
- 若目标目录已存在,会进行交互确认;按回车或输入 `y/yes` 继续,输入 `n` 取消
|
|
35
|
-
|
|
36
|
-
示例(本地构建后运行):
|
|
37
|
-
|
|
38
|
-
```bash
|
|
39
|
-
node dist/main.cjs
|
|
40
|
-
# 请输入或粘贴需要处理的源文件夹路径: C:\Users\me\Desktop\Notes
|
|
41
|
-
```
|
|
42
|
-
|
|
43
|
-
## 处理策略
|
|
44
|
-
|
|
45
|
-
- 下载与重写
|
|
46
|
-
- 遍历 Markdown AST 中的 `image` 节点,仅处理 `http` 开头的链接
|
|
47
|
-
- 保存为 `assets/<hash>.<ext>` 并将 Markdown 中的 URL 改为相对路径 `./assets/<hash>.<ext>`
|
|
48
|
-
- 并发与大图串行
|
|
49
|
-
- 总并发槽位数为 5
|
|
50
|
-
- 普通图片占用 1 槽位并发下载
|
|
51
|
-
- 超大图片(> 20MB)会“独占”全部槽位,串行下载与压缩,避免内存与带宽拥塞
|
|
52
|
-
- 压缩与格式
|
|
53
|
-
- 基准目标:将图片控制在 10MB 内
|
|
54
|
-
- 首先尝试转 WebP(quality≈80);仍超出则缩放至最大宽度 2560 并再压缩;最后“保底”极限压缩(quality≈60)
|
|
55
|
-
- 如果发生压缩/转码,最终扩展名会改为 `.webp`;SVG 不参与压缩与改码
|
|
56
|
-
- 进度与日志
|
|
57
|
-
- 使用优雅的命令行进度动画显示当前下载项目与累计进度
|
|
58
|
-
- 处理完成后在输出目录生成 `image-log-*.txt`,记录成功/失败、尺寸变化与汇总统计
|
|
59
|
-
|
|
60
|
-
## 常见问答
|
|
61
|
-
|
|
62
|
-
- 已有本地图片会被重复处理吗?不会,工具仅处理 `http` 链接。
|
|
63
|
-
- 动图是否保留?在支持的情况下,WebP 保留原始 GIF/APNG 的动画(由 `sharp` 处理)。
|
|
64
|
-
- 我能调整并发或压缩阈值吗?可以,修改源码中的常量后重新构建:
|
|
65
|
-
- 并发与大图阈值:[`Semaphore.ts`](file:///c:/Users/37524/Desktop/md-img-pull/utils/Semaphore.ts)
|
|
66
|
-
- 压缩目标与质量:[`imageCompression.ts`](file:///c:/Users/37524/Desktop/md-img-pull/utils/imageCompression.ts)
|
|
67
|
-
|
|
68
|
-
## 代码入口与关键模块
|
|
69
|
-
|
|
70
|
-
- CLI 入口与交互:[`main.ts`](file:///c:/Users/37524/Desktop/md-img-pull/main.ts)
|
|
71
|
-
- Markdown 解析与图片节点收集:[`processSingleMarkdown.ts`](file:///c:/Users/37524/Desktop/md-img-pull/utils/processSingleMarkdown.ts)
|
|
72
|
-
- 下载、本地化与重写:[`downloader.ts`](file:///c:/Users/37524/Desktop/md-img-pull/utils/downloader.ts)
|
|
73
|
-
- 并发控制(信号量):[`Semaphore.ts`](file:///c:/Users/37524/Desktop/md-img-pull/utils/Semaphore.ts)
|
|
74
|
-
- 下载进度动画:[`downloadProgress.ts`](file:///c:/Users/37524/Desktop/md-img-pull/utils/downloadProgress.ts)
|
|
75
|
-
- 处理日志管理:[`imageLog.ts`](file:///c:/Users/37524/Desktop/md-img-pull/utils/imageLog.ts)
|
|
76
|
-
|
|
77
|
-
## 构建与发布
|
|
78
|
-
|
|
79
|
-
- 使用 `esbuild` 打包为 CJS,并通过 `bin` 暴露命令 `md-img-p`
|
|
80
|
-
- 打包脚本:`pnpm build`
|
|
81
|
-
- 包配置与入口:[`package.json`](file:///c:/Users/37524/Desktop/md-img-pull/package.json)
|
|
82
|
-
|
|
83
|
-
## 许可
|
|
84
|
-
|
|
85
|
-
ISC License
|
|
1
|
+
# md-img-pull
|
|
2
|
+
|
|
3
|
+
一个用于将 Markdown 中的网络图片下载到本地、统一重写引用路径并对超大图片进行压缩的命令行工具。适合将笔记、博客中的媒体资源“本地化”,让内容更可移植、更易备份。
|
|
4
|
+
|
|
5
|
+
## 功能概述
|
|
6
|
+
|
|
7
|
+
- 递归扫描文件夹或处理单个 `.md` 文件
|
|
8
|
+
- 仅处理以 `http` 开头的图片链接,已本地化的相对/本地路径会跳过
|
|
9
|
+
- 将图片保存到目标 Markdown 同级的 `assets/` 目录,文件名为内容哈希
|
|
10
|
+
- 对超过阈值的图片自动压缩,必要时统一转为 WebP,尽量保留动画
|
|
11
|
+
- 使用并发控制与进度动画,清晰显示下载/压缩过程
|
|
12
|
+
- 输出详细的处理日志(成功/失败、尺寸变化)到目标目录
|
|
13
|
+
|
|
14
|
+
## 安装
|
|
15
|
+
|
|
16
|
+
- 全局安装(如果已发布到 npm):
|
|
17
|
+
- `npm i -g @duoyun/md-img-pull` 或 `pnpm add -g @duoyun/md-img-pull`
|
|
18
|
+
- 命令:`md-img-p`
|
|
19
|
+
- 本地构建运行(当前仓库):
|
|
20
|
+
- 安装依赖:`pnpm i`
|
|
21
|
+
- 构建:`pnpm build`
|
|
22
|
+
- 运行:`node dist/main.cjs`
|
|
23
|
+
|
|
24
|
+
环境要求:推荐 Node.js ≥ 18。依赖 `sharp`,在常见平台会自动下载所需的 libvips。
|
|
25
|
+
|
|
26
|
+
## 使用说明
|
|
27
|
+
|
|
28
|
+
- 运行后按提示输入源路径,可以是:
|
|
29
|
+
- 某个 Markdown 文件路径,例如 `C:\Notes\foo.md`
|
|
30
|
+
- 某个目录路径,例如 `C:\Notes`
|
|
31
|
+
- 输出位置:
|
|
32
|
+
- 单文件模式:在同目录生成 `foo_localized/`,并包含 `assets/`
|
|
33
|
+
- 目录模式:在同级生成 `Notes_localized/`,保持原有层级结构与 `assets/`
|
|
34
|
+
- 若目标目录已存在,会进行交互确认;按回车或输入 `y/yes` 继续,输入 `n` 取消
|
|
35
|
+
|
|
36
|
+
示例(本地构建后运行):
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
node dist/main.cjs
|
|
40
|
+
# 请输入或粘贴需要处理的源文件夹路径: C:\Users\me\Desktop\Notes
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## 处理策略
|
|
44
|
+
|
|
45
|
+
- 下载与重写
|
|
46
|
+
- 遍历 Markdown AST 中的 `image` 节点,仅处理 `http` 开头的链接
|
|
47
|
+
- 保存为 `assets/<hash>.<ext>` 并将 Markdown 中的 URL 改为相对路径 `./assets/<hash>.<ext>`
|
|
48
|
+
- 并发与大图串行
|
|
49
|
+
- 总并发槽位数为 5
|
|
50
|
+
- 普通图片占用 1 槽位并发下载
|
|
51
|
+
- 超大图片(> 20MB)会“独占”全部槽位,串行下载与压缩,避免内存与带宽拥塞
|
|
52
|
+
- 压缩与格式
|
|
53
|
+
- 基准目标:将图片控制在 10MB 内
|
|
54
|
+
- 首先尝试转 WebP(quality≈80);仍超出则缩放至最大宽度 2560 并再压缩;最后“保底”极限压缩(quality≈60)
|
|
55
|
+
- 如果发生压缩/转码,最终扩展名会改为 `.webp`;SVG 不参与压缩与改码
|
|
56
|
+
- 进度与日志
|
|
57
|
+
- 使用优雅的命令行进度动画显示当前下载项目与累计进度
|
|
58
|
+
- 处理完成后在输出目录生成 `image-log-*.txt`,记录成功/失败、尺寸变化与汇总统计
|
|
59
|
+
|
|
60
|
+
## 常见问答
|
|
61
|
+
|
|
62
|
+
- 已有本地图片会被重复处理吗?不会,工具仅处理 `http` 链接。
|
|
63
|
+
- 动图是否保留?在支持的情况下,WebP 保留原始 GIF/APNG 的动画(由 `sharp` 处理)。
|
|
64
|
+
- 我能调整并发或压缩阈值吗?可以,修改源码中的常量后重新构建:
|
|
65
|
+
- 并发与大图阈值:[`Semaphore.ts`](file:///c:/Users/37524/Desktop/md-img-pull/utils/Semaphore.ts)
|
|
66
|
+
- 压缩目标与质量:[`imageCompression.ts`](file:///c:/Users/37524/Desktop/md-img-pull/utils/imageCompression.ts)
|
|
67
|
+
|
|
68
|
+
## 代码入口与关键模块
|
|
69
|
+
|
|
70
|
+
- CLI 入口与交互:[`main.ts`](file:///c:/Users/37524/Desktop/md-img-pull/main.ts)
|
|
71
|
+
- Markdown 解析与图片节点收集:[`processSingleMarkdown.ts`](file:///c:/Users/37524/Desktop/md-img-pull/utils/processSingleMarkdown.ts)
|
|
72
|
+
- 下载、本地化与重写:[`downloader.ts`](file:///c:/Users/37524/Desktop/md-img-pull/utils/downloader.ts)
|
|
73
|
+
- 并发控制(信号量):[`Semaphore.ts`](file:///c:/Users/37524/Desktop/md-img-pull/utils/Semaphore.ts)
|
|
74
|
+
- 下载进度动画:[`downloadProgress.ts`](file:///c:/Users/37524/Desktop/md-img-pull/utils/downloadProgress.ts)
|
|
75
|
+
- 处理日志管理:[`imageLog.ts`](file:///c:/Users/37524/Desktop/md-img-pull/utils/imageLog.ts)
|
|
76
|
+
|
|
77
|
+
## 构建与发布
|
|
78
|
+
|
|
79
|
+
- 使用 `esbuild` 打包为 CJS,并通过 `bin` 暴露命令 `md-img-p`
|
|
80
|
+
- 打包脚本:`pnpm build`
|
|
81
|
+
- 包配置与入口:[`package.json`](file:///c:/Users/37524/Desktop/md-img-pull/package.json)
|
|
82
|
+
|
|
83
|
+
## 许可
|
|
84
|
+
|
|
85
|
+
ISC License
|
package/dist/main.cjs
CHANGED
|
@@ -30846,64 +30846,74 @@ var DownloadProgressManager = class {
|
|
|
30846
30846
|
};
|
|
30847
30847
|
var downloadProgress = new DownloadProgressManager();
|
|
30848
30848
|
|
|
30849
|
-
// utils/
|
|
30850
|
-
var
|
|
30851
|
-
|
|
30852
|
-
|
|
30853
|
-
|
|
30854
|
-
|
|
30855
|
-
|
|
30856
|
-
timer = null;
|
|
30857
|
-
dots = "";
|
|
30858
|
-
direction = 1;
|
|
30859
|
-
// 1 = 增加, -1 = 减少
|
|
30860
|
-
spinner;
|
|
30861
|
-
baseText;
|
|
30862
|
-
constructor(spinner, baseText) {
|
|
30863
|
-
this.spinner = spinner;
|
|
30864
|
-
this.baseText = baseText;
|
|
30865
|
-
}
|
|
30849
|
+
// utils/compressionProgress.ts
|
|
30850
|
+
var CompressionProgressManager = class {
|
|
30851
|
+
spinner = null;
|
|
30852
|
+
activeCount = 0;
|
|
30853
|
+
// 当前正在压缩的任务数
|
|
30854
|
+
currentTask = "";
|
|
30855
|
+
// 当前显示的任务信息
|
|
30866
30856
|
/**
|
|
30867
|
-
*
|
|
30868
|
-
* @param
|
|
30857
|
+
* 开始一个压缩任务
|
|
30858
|
+
* @param taskInfo 任务信息(如 "10.81MB → 转换为 WebP")
|
|
30859
|
+
* @returns 更新函数,用于更新当前任务的进度
|
|
30869
30860
|
*/
|
|
30870
|
-
start(
|
|
30871
|
-
this.
|
|
30872
|
-
this.
|
|
30873
|
-
|
|
30874
|
-
|
|
30875
|
-
|
|
30876
|
-
|
|
30877
|
-
|
|
30878
|
-
|
|
30861
|
+
start(taskInfo) {
|
|
30862
|
+
this.activeCount++;
|
|
30863
|
+
if (this.activeCount === 1) {
|
|
30864
|
+
downloadProgress.pause();
|
|
30865
|
+
this.currentTask = taskInfo;
|
|
30866
|
+
this.spinner = ora({
|
|
30867
|
+
text: `\u538B\u7F29\u4E2D: ${taskInfo}`,
|
|
30868
|
+
spinner: "dots",
|
|
30869
|
+
color: "yellow"
|
|
30870
|
+
}).start();
|
|
30871
|
+
}
|
|
30872
|
+
const updateFn = (newInfo) => {
|
|
30873
|
+
if (this.spinner && this.currentTask === taskInfo) {
|
|
30874
|
+
this.currentTask = newInfo;
|
|
30875
|
+
this.spinner.text = `\u538B\u7F29\u4E2D: ${newInfo}`;
|
|
30879
30876
|
}
|
|
30880
|
-
|
|
30881
|
-
|
|
30882
|
-
}
|
|
30883
|
-
/**
|
|
30884
|
-
* 更新显示文字
|
|
30885
|
-
*/
|
|
30886
|
-
updateText() {
|
|
30887
|
-
const paddedDots = this.dots.padEnd(3, " ");
|
|
30888
|
-
this.spinner.text = `${this.baseText}${paddedDots}`;
|
|
30877
|
+
};
|
|
30878
|
+
return updateFn;
|
|
30889
30879
|
}
|
|
30890
30880
|
/**
|
|
30891
|
-
*
|
|
30881
|
+
* 完成一个压缩任务
|
|
30882
|
+
* @param successMsg 成功消息
|
|
30892
30883
|
*/
|
|
30893
|
-
|
|
30894
|
-
this.
|
|
30895
|
-
this.
|
|
30884
|
+
complete(successMsg) {
|
|
30885
|
+
this.activeCount--;
|
|
30886
|
+
if (this.activeCount === 0 && this.spinner) {
|
|
30887
|
+
this.spinner.succeed(successMsg);
|
|
30888
|
+
this.spinner = null;
|
|
30889
|
+
downloadProgress.resume();
|
|
30890
|
+
}
|
|
30896
30891
|
}
|
|
30897
30892
|
/**
|
|
30898
|
-
*
|
|
30893
|
+
* 压缩任务失败
|
|
30894
|
+
* @param errorMsg 错误消息
|
|
30899
30895
|
*/
|
|
30900
|
-
|
|
30901
|
-
|
|
30902
|
-
|
|
30903
|
-
this.
|
|
30896
|
+
fail(errorMsg) {
|
|
30897
|
+
this.activeCount--;
|
|
30898
|
+
if (this.activeCount === 0 && this.spinner) {
|
|
30899
|
+
this.spinner.fail(errorMsg);
|
|
30900
|
+
this.spinner = null;
|
|
30901
|
+
downloadProgress.resume();
|
|
30904
30902
|
}
|
|
30905
30903
|
}
|
|
30906
30904
|
};
|
|
30905
|
+
var compressionProgress = new CompressionProgressManager();
|
|
30906
|
+
|
|
30907
|
+
// utils/imageCompression.ts
|
|
30908
|
+
var SHARP_OPTIONS = {
|
|
30909
|
+
animated: true,
|
|
30910
|
+
limitInputPixels: false,
|
|
30911
|
+
sequentialRead: true
|
|
30912
|
+
};
|
|
30913
|
+
async function convertToWebp(inputBuffer) {
|
|
30914
|
+
const image2 = (0, import_sharp.default)(inputBuffer, SHARP_OPTIONS);
|
|
30915
|
+
return await image2.webp({ quality: 80, effort: 6 }).toBuffer();
|
|
30916
|
+
}
|
|
30907
30917
|
async function compressImage(inputBuffer) {
|
|
30908
30918
|
const MAX_SIZE = 10 * 1024 * 1024;
|
|
30909
30919
|
const image2 = (0, import_sharp.default)(inputBuffer, SHARP_OPTIONS);
|
|
@@ -30912,41 +30922,24 @@ async function compressImage(inputBuffer) {
|
|
|
30912
30922
|
return inputBuffer;
|
|
30913
30923
|
}
|
|
30914
30924
|
const originalSize = (inputBuffer.length / 1024 / 1024).toFixed(2);
|
|
30915
|
-
|
|
30916
|
-
const
|
|
30917
|
-
text: `\u538B\u7F29\u4E2D: ${originalSize}MB \u2192 \u8F6C\u6362\u4E3A WebP`,
|
|
30918
|
-
spinner: "dots",
|
|
30919
|
-
color: "yellow"
|
|
30920
|
-
}).start();
|
|
30921
|
-
const dynamicDots = new DynamicDots(
|
|
30922
|
-
spinner,
|
|
30923
|
-
`\u538B\u7F29\u4E2D: ${originalSize}MB \u2192 \u8F6C\u6362\u4E3A WebP`
|
|
30924
|
-
);
|
|
30925
|
-
dynamicDots.start(300);
|
|
30925
|
+
const taskInfo = `${originalSize}MB \u2192 \u8F6C\u6362\u4E3A WebP`;
|
|
30926
|
+
const updateProgress = compressionProgress.start(taskInfo);
|
|
30926
30927
|
try {
|
|
30927
30928
|
let currentBuffer = await image2.webp({ quality: 80, effort: 6 }).toBuffer();
|
|
30928
30929
|
if (currentBuffer.length > MAX_SIZE) {
|
|
30929
|
-
|
|
30930
|
-
`\u538B\u7F29\u4E2D: ${originalSize}MB \u2192 \u7F29\u5C0F\u5206\u8FA8\u7387\u81F3 2560px`
|
|
30931
|
-
);
|
|
30930
|
+
updateProgress(`${originalSize}MB \u2192 \u7F29\u5C0F\u5206\u8FA8\u7387\u81F3 2560px`);
|
|
30932
30931
|
currentBuffer = await (0, import_sharp.default)(inputBuffer, SHARP_OPTIONS).resize(2560, void 0, { withoutEnlargement: true }).webp({ quality: 75 }).toBuffer();
|
|
30933
30932
|
}
|
|
30934
30933
|
if (currentBuffer.length > MAX_SIZE) {
|
|
30935
|
-
|
|
30936
|
-
`\u538B\u7F29\u4E2D: ${originalSize}MB \u2192 \u6781\u9650\u538B\u7F29 (quality: 60)`
|
|
30937
|
-
);
|
|
30934
|
+
updateProgress(`${originalSize}MB \u2192 \u6781\u9650\u538B\u7F29 (quality: 60)`);
|
|
30938
30935
|
currentBuffer = await (0, import_sharp.default)(currentBuffer, SHARP_OPTIONS).webp({ quality: 60 }).toBuffer();
|
|
30939
30936
|
}
|
|
30940
|
-
dynamicDots.stop();
|
|
30941
30937
|
const finalSize = (currentBuffer.length / 1024 / 1024).toFixed(2);
|
|
30942
|
-
|
|
30938
|
+
compressionProgress.complete(`\u538B\u7F29\u5B8C\u6210: ${originalSize}MB \u2192 ${finalSize}MB`);
|
|
30943
30939
|
return currentBuffer;
|
|
30944
30940
|
} catch (error2) {
|
|
30945
|
-
|
|
30946
|
-
spinner.fail(`\u538B\u7F29\u5931\u8D25: ${error2}`);
|
|
30941
|
+
compressionProgress.fail(`\u538B\u7F29\u5931\u8D25: ${error2}`);
|
|
30947
30942
|
throw error2;
|
|
30948
|
-
} finally {
|
|
30949
|
-
downloadProgress.resume();
|
|
30950
30943
|
}
|
|
30951
30944
|
}
|
|
30952
30945
|
|
|
@@ -31124,19 +31117,42 @@ var mimeMap = {
|
|
|
31124
31117
|
"image/jpeg": ".jpg",
|
|
31125
31118
|
"image/png": ".png",
|
|
31126
31119
|
"image/gif": ".gif",
|
|
31127
|
-
"image/svg+xml": ".svg"
|
|
31120
|
+
"image/svg+xml": ".svg",
|
|
31121
|
+
"image/webp": ".webp"
|
|
31128
31122
|
};
|
|
31129
31123
|
async function downloadAndLocalize(node2, assetDir) {
|
|
31130
31124
|
const currentUrl = node2.url;
|
|
31131
31125
|
if (!currentUrl) return;
|
|
31132
|
-
|
|
31133
|
-
|
|
31126
|
+
let requiredPermits = 1;
|
|
31127
|
+
try {
|
|
31128
|
+
const headResponse = await axios_default.head(currentUrl, { timeout: 5e3 });
|
|
31129
|
+
const contentLength = parseInt(
|
|
31130
|
+
headResponse.headers["content-length"] || "0",
|
|
31131
|
+
10
|
|
31132
|
+
);
|
|
31133
|
+
if (contentLength > LARGE_IMAGE_THRESHOLD) {
|
|
31134
|
+
requiredPermits = TOTAL_PERMITS;
|
|
31135
|
+
}
|
|
31136
|
+
} catch {
|
|
31137
|
+
}
|
|
31138
|
+
await downloadSemaphore.acquire(requiredPermits);
|
|
31139
|
+
let heldPermits = requiredPermits;
|
|
31134
31140
|
try {
|
|
31135
31141
|
const hash = import_crypto2.default.createHash("md5").update(currentUrl).digest("hex");
|
|
31136
|
-
const
|
|
31137
|
-
|
|
31138
|
-
|
|
31139
|
-
|
|
31142
|
+
const controller = new AbortController();
|
|
31143
|
+
const downloadTimeout = 6e4;
|
|
31144
|
+
const timeoutId = setTimeout(() => controller.abort(), downloadTimeout);
|
|
31145
|
+
let response;
|
|
31146
|
+
try {
|
|
31147
|
+
response = await axios_default.get(currentUrl, {
|
|
31148
|
+
responseType: "arraybuffer",
|
|
31149
|
+
timeout: 1e4,
|
|
31150
|
+
// 连接超时 10 秒
|
|
31151
|
+
signal: controller.signal
|
|
31152
|
+
});
|
|
31153
|
+
} finally {
|
|
31154
|
+
clearTimeout(timeoutId);
|
|
31155
|
+
}
|
|
31140
31156
|
const contentType = response.headers["content-type"];
|
|
31141
31157
|
let finalExt = mimeMap[contentType];
|
|
31142
31158
|
if (!finalExt) {
|
|
@@ -31146,14 +31162,12 @@ async function downloadAndLocalize(node2, assetDir) {
|
|
|
31146
31162
|
}
|
|
31147
31163
|
let imageData = Buffer.from(response.data);
|
|
31148
31164
|
const MAX_SIZE = 10 * 1024 * 1024;
|
|
31149
|
-
if (
|
|
31150
|
-
if (imageData.length >
|
|
31151
|
-
await
|
|
31152
|
-
|
|
31165
|
+
if (contentType !== "image/svg+xml") {
|
|
31166
|
+
if (imageData.length > MAX_SIZE) {
|
|
31167
|
+
imageData = await compressImage(imageData);
|
|
31168
|
+
} else {
|
|
31169
|
+
imageData = await convertToWebp(imageData);
|
|
31153
31170
|
}
|
|
31154
|
-
imageData = await compressImage(imageData);
|
|
31155
|
-
}
|
|
31156
|
-
if (imageData.length !== response.data.byteLength) {
|
|
31157
31171
|
finalExt = ".webp";
|
|
31158
31172
|
}
|
|
31159
31173
|
const fileName = `${hash}${finalExt}`;
|
package/package.json
CHANGED
|
@@ -1,51 +1,51 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@duoyun/md-img-pull",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "Markdown 媒体资源本地化与压缩工具",
|
|
5
|
-
"main": "./dist/main.cjs",
|
|
6
|
-
"repository": {
|
|
7
|
-
"type": "git",
|
|
8
|
-
"url": "git+https://github.com/518luck/md-img-pull.git"
|
|
9
|
-
},
|
|
10
|
-
"homepage": "https://github.com/518luck/md-img-pull#readme",
|
|
11
|
-
"publishConfig": {
|
|
12
|
-
"access": "public",
|
|
13
|
-
"registry": "https://registry.npmjs.org/"
|
|
14
|
-
},
|
|
15
|
-
"bin": {
|
|
16
|
-
"md-img-p": "dist/main.cjs"
|
|
17
|
-
},
|
|
18
|
-
"type": "module",
|
|
19
|
-
"files": [
|
|
20
|
-
"dist"
|
|
21
|
-
],
|
|
22
|
-
"scripts": {
|
|
23
|
-
"build": "esbuild main.ts --bundle --platform=node --format=cjs --outfile=dist/main.cjs --external:sharp --banner:js=\"#!/usr/bin/env node\"",
|
|
24
|
-
"test": "echo \"Error: no test specified\" && exit 1"
|
|
25
|
-
},
|
|
26
|
-
"keywords": [],
|
|
27
|
-
"author": "",
|
|
28
|
-
"license": "ISC",
|
|
29
|
-
"packageManager": "pnpm@10.28.0",
|
|
30
|
-
"devDependencies": {
|
|
31
|
-
"@types/fs-extra": "^11.0.4",
|
|
32
|
-
"@types/node": "^25.2.1",
|
|
33
|
-
"esbuild": "^0.27.3",
|
|
34
|
-
"ts-node": "^10.9.2",
|
|
35
|
-
"typescript": "^5.9.3"
|
|
36
|
-
},
|
|
37
|
-
"dependencies": {
|
|
38
|
-
"@types/mdast": "^4.0.4",
|
|
39
|
-
"axios": "^1.13.4",
|
|
40
|
-
"chalk": "^5.6.2",
|
|
41
|
-
"crypto": "^1.0.1",
|
|
42
|
-
"fs-extra": "^11.3.3",
|
|
43
|
-
"ora": "^9.3.0",
|
|
44
|
-
"p-limit": "^7.3.0",
|
|
45
|
-
"remark-parse": "^11.0.0",
|
|
46
|
-
"remark-stringify": "^11.0.0",
|
|
47
|
-
"sharp": "^0.34.5",
|
|
48
|
-
"unified": "^11.0.5",
|
|
49
|
-
"unist-util-visit": "^5.1.0"
|
|
50
|
-
}
|
|
51
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "@duoyun/md-img-pull",
|
|
3
|
+
"version": "1.1.0",
|
|
4
|
+
"description": "Markdown 媒体资源本地化与压缩工具",
|
|
5
|
+
"main": "./dist/main.cjs",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/518luck/md-img-pull.git"
|
|
9
|
+
},
|
|
10
|
+
"homepage": "https://github.com/518luck/md-img-pull#readme",
|
|
11
|
+
"publishConfig": {
|
|
12
|
+
"access": "public",
|
|
13
|
+
"registry": "https://registry.npmjs.org/"
|
|
14
|
+
},
|
|
15
|
+
"bin": {
|
|
16
|
+
"md-img-p": "dist/main.cjs"
|
|
17
|
+
},
|
|
18
|
+
"type": "module",
|
|
19
|
+
"files": [
|
|
20
|
+
"dist"
|
|
21
|
+
],
|
|
22
|
+
"scripts": {
|
|
23
|
+
"build": "esbuild main.ts --bundle --platform=node --format=cjs --outfile=dist/main.cjs --external:sharp --banner:js=\"#!/usr/bin/env node\"",
|
|
24
|
+
"test": "echo \"Error: no test specified\" && exit 1"
|
|
25
|
+
},
|
|
26
|
+
"keywords": [],
|
|
27
|
+
"author": "",
|
|
28
|
+
"license": "ISC",
|
|
29
|
+
"packageManager": "pnpm@10.28.0",
|
|
30
|
+
"devDependencies": {
|
|
31
|
+
"@types/fs-extra": "^11.0.4",
|
|
32
|
+
"@types/node": "^25.2.1",
|
|
33
|
+
"esbuild": "^0.27.3",
|
|
34
|
+
"ts-node": "^10.9.2",
|
|
35
|
+
"typescript": "^5.9.3"
|
|
36
|
+
},
|
|
37
|
+
"dependencies": {
|
|
38
|
+
"@types/mdast": "^4.0.4",
|
|
39
|
+
"axios": "^1.13.4",
|
|
40
|
+
"chalk": "^5.6.2",
|
|
41
|
+
"crypto": "^1.0.1",
|
|
42
|
+
"fs-extra": "^11.3.3",
|
|
43
|
+
"ora": "^9.3.0",
|
|
44
|
+
"p-limit": "^7.3.0",
|
|
45
|
+
"remark-parse": "^11.0.0",
|
|
46
|
+
"remark-stringify": "^11.0.0",
|
|
47
|
+
"sharp": "^0.34.5",
|
|
48
|
+
"unified": "^11.0.5",
|
|
49
|
+
"unist-util-visit": "^5.1.0"
|
|
50
|
+
}
|
|
51
|
+
}
|