pdf-fetch 0.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/LICENSE +22 -0
- package/README.md +58 -0
- package/dist/cli.js +197 -0
- package/dist/cli.js.map +1 -0
- package/package.json +53 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
22
|
+
|
package/README.md
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# pdf-fetch(CLI:pdf2jpg)
|
|
2
|
+
|
|
3
|
+
一个简单的命令行工具:使用 **ImageMagick(magick)** 从 PDF 中提取页面并导出为 JPG。
|
|
4
|
+
|
|
5
|
+
## 前置条件
|
|
6
|
+
|
|
7
|
+
- macOS(你当前需求场景)
|
|
8
|
+
- 已安装 ImageMagick,并确保命令 `magick` 可用
|
|
9
|
+
|
|
10
|
+
## 安装
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
pnpm add -g pdf-fetch
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## 使用
|
|
17
|
+
|
|
18
|
+
无参数时会输出中文使用说明:
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
pdf2jpg
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
转换整个 PDF:
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
pdf2jpg "name.pdf"
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
指定分辨率/质量:
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
pdf2jpg "name.pdf" -d 200 -q 100
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
指定输出文件名前缀 + 指定页码:
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
pdf2jpg "name.pdf" -n "new" -p 10-15,20
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## 行为规则
|
|
43
|
+
|
|
44
|
+
- 在 PDF 所在目录创建同名文件夹(去掉 `.pdf` 扩展名),例如 `name.pdf` → `./name/`
|
|
45
|
+
- 默认导出所有页面
|
|
46
|
+
- 默认 DPI:150(`-d 200` 可改)
|
|
47
|
+
- 默认 JPG 质量:95(`-q 100` 可改)
|
|
48
|
+
- 默认文件名:`p01.jpg, p02.jpg ...`,序号从 `01` 开始(支持 `-n` 修改前缀,例如 `new01.jpg`)
|
|
49
|
+
- 如果目标 JPG 文件已存在:**报错退出**(符合你的覆盖策略要求)
|
|
50
|
+
|
|
51
|
+
## 开发
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
pnpm i
|
|
55
|
+
pnpm run lint
|
|
56
|
+
pnpm run typecheck
|
|
57
|
+
pnpm run build
|
|
58
|
+
```
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
var __create = Object.create;
|
|
4
|
+
var __defProp = Object.defineProperty;
|
|
5
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
6
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
7
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
8
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
9
|
+
var __copyProps = (to, from, except, desc) => {
|
|
10
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
11
|
+
for (let key of __getOwnPropNames(from))
|
|
12
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
13
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
14
|
+
}
|
|
15
|
+
return to;
|
|
16
|
+
};
|
|
17
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
18
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
19
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
20
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
21
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
22
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
23
|
+
mod
|
|
24
|
+
));
|
|
25
|
+
|
|
26
|
+
// src/cli.ts
|
|
27
|
+
var import_commander = require("commander");
|
|
28
|
+
var import_node_child_process = require("child_process");
|
|
29
|
+
var import_node_fs = require("fs");
|
|
30
|
+
var import_promises = require("fs/promises");
|
|
31
|
+
var import_node_path = __toESM(require("path"));
|
|
32
|
+
var import_node_util = require("util");
|
|
33
|
+
var execFileAsync = (0, import_node_util.promisify)(import_node_child_process.execFile);
|
|
34
|
+
function printChineseUsage() {
|
|
35
|
+
const text = `
|
|
36
|
+
\u7528\u6CD5\uFF1A
|
|
37
|
+
pdf2jpg "\u6587\u4EF6.pdf" [\u9009\u9879]
|
|
38
|
+
|
|
39
|
+
\u8BF4\u660E\uFF1A
|
|
40
|
+
- \u5728 PDF \u6240\u5728\u76EE\u5F55\u521B\u5EFA\u540C\u540D\u6587\u4EF6\u5939\uFF08\u53BB\u6389 .pdf \u6269\u5C55\u540D\uFF09
|
|
41
|
+
- \u5C06\u6307\u5B9A\u9875\u9762\u5BFC\u51FA\u4E3A JPG\uFF0C\u6587\u4EF6\u540D\u89C4\u5219\uFF1A\u524D\u7F00 + \u5E8F\u53F7\uFF08\u4ECE 01 \u5F00\u59CB\uFF09
|
|
42
|
+
|
|
43
|
+
\u9009\u9879\uFF1A
|
|
44
|
+
-d, --dpi <\u6570\u5B57> \u5206\u8FA8\u7387\uFF08DPI\uFF09\uFF0C\u9ED8\u8BA4 150
|
|
45
|
+
-q, --quality <\u6570\u5B57> JPG \u8D28\u91CF\uFF0C\u9ED8\u8BA4 95
|
|
46
|
+
-n, --name <\u524D\u7F00> \u6587\u4EF6\u540D\u524D\u7F00\uFF0C\u9ED8\u8BA4 p\uFF08\u793A\u4F8B\uFF1Ap01.jpg\uFF09
|
|
47
|
+
-p, --pages <\u8303\u56F4> \u9009\u62E9\u9875\u9762\uFF1A
|
|
48
|
+
-p 10 \u7B2C 10 \u9875
|
|
49
|
+
-p 10,12,13 \u7B2C 10/12/13 \u9875
|
|
50
|
+
-p 10-15 \u7B2C 10 \u5230 15 \u9875
|
|
51
|
+
-p 10-15,20 \u7B2C 10-15 \u9875\u548C\u7B2C 20 \u9875
|
|
52
|
+
|
|
53
|
+
\u793A\u4F8B\uFF1A
|
|
54
|
+
pdf2jpg "name.pdf"
|
|
55
|
+
pdf2jpg "name.pdf" -d 200 -q 100
|
|
56
|
+
pdf2jpg "name.pdf" -n "new" -p 10-15,20
|
|
57
|
+
`;
|
|
58
|
+
console.log(text.trim());
|
|
59
|
+
}
|
|
60
|
+
function padNumber(n, width) {
|
|
61
|
+
return String(n).padStart(width, "0");
|
|
62
|
+
}
|
|
63
|
+
function parsePagesSpec(spec) {
|
|
64
|
+
const raw = spec.split(",").map((s) => s.trim()).filter(Boolean);
|
|
65
|
+
const pages = [];
|
|
66
|
+
for (const part of raw) {
|
|
67
|
+
const rangeMatch = part.match(/^(\d+)\s*-\s*(\d+)$/);
|
|
68
|
+
if (rangeMatch) {
|
|
69
|
+
const start = Number(rangeMatch[1]);
|
|
70
|
+
const end = Number(rangeMatch[2]);
|
|
71
|
+
if (!Number.isInteger(start) || !Number.isInteger(end) || start <= 0 || end <= 0) {
|
|
72
|
+
throw new Error(`\u9875\u7801\u8303\u56F4\u975E\u6CD5\uFF1A${part}`);
|
|
73
|
+
}
|
|
74
|
+
if (start > end) {
|
|
75
|
+
throw new Error(`\u9875\u7801\u8303\u56F4\u8D77\u6B62\u987A\u5E8F\u9519\u8BEF\uFF1A${part}`);
|
|
76
|
+
}
|
|
77
|
+
for (let i = start; i <= end; i += 1) pages.push(i);
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
const singleMatch = part.match(/^\d+$/);
|
|
81
|
+
if (singleMatch) {
|
|
82
|
+
const v = Number(part);
|
|
83
|
+
if (!Number.isInteger(v) || v <= 0) throw new Error(`\u9875\u7801\u975E\u6CD5\uFF1A${part}`);
|
|
84
|
+
pages.push(v);
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
throw new Error(`\u65E0\u6CD5\u89E3\u6790\u9875\u7801\u53C2\u6570\uFF1A${part}`);
|
|
88
|
+
}
|
|
89
|
+
return Array.from(new Set(pages)).sort((a, b) => a - b);
|
|
90
|
+
}
|
|
91
|
+
async function ensureMagickAvailable() {
|
|
92
|
+
try {
|
|
93
|
+
await execFileAsync("magick", ["-version"]);
|
|
94
|
+
} catch {
|
|
95
|
+
throw new Error("\u672A\u68C0\u6D4B\u5230 ImageMagick \u7684 magick \u547D\u4EE4\u3002\u8BF7\u786E\u8BA4\u5DF2\u5B89\u88C5\u5E76\u4E14\u5728 PATH \u4E2D\u53EF\u7528\u3002");
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
async function getPdfPageCount(pdfPath) {
|
|
99
|
+
const { stdout } = await execFileAsync("magick", ["identify", "-ping", pdfPath], {
|
|
100
|
+
maxBuffer: 10 * 1024 * 1024
|
|
101
|
+
});
|
|
102
|
+
const lines = stdout.split(/\r?\n/).map((s) => s.trim()).filter(Boolean);
|
|
103
|
+
if (lines.length === 0) throw new Error("\u65E0\u6CD5\u8BC6\u522B PDF \u9875\u6570\uFF08magick identify \u8F93\u51FA\u4E3A\u7A7A\uFF09\u3002");
|
|
104
|
+
return lines.length;
|
|
105
|
+
}
|
|
106
|
+
async function convertSinglePageToJpg(params) {
|
|
107
|
+
const magickPageIndex = params.pageNumber - 1;
|
|
108
|
+
await execFileAsync(
|
|
109
|
+
"magick",
|
|
110
|
+
[
|
|
111
|
+
"-density",
|
|
112
|
+
String(params.dpi),
|
|
113
|
+
`${params.pdfPath}[${magickPageIndex}]`,
|
|
114
|
+
"-quality",
|
|
115
|
+
String(params.quality),
|
|
116
|
+
params.outputPath
|
|
117
|
+
],
|
|
118
|
+
{ maxBuffer: 10 * 1024 * 1024 }
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
async function run(inputPdf, options) {
|
|
122
|
+
if (!(0, import_node_fs.existsSync)(inputPdf)) {
|
|
123
|
+
throw new Error(`\u627E\u4E0D\u5230\u6587\u4EF6\uFF1A${inputPdf}`);
|
|
124
|
+
}
|
|
125
|
+
await ensureMagickAvailable();
|
|
126
|
+
const pdfAbsPath = import_node_path.default.resolve(process.cwd(), inputPdf);
|
|
127
|
+
const pdfDir = import_node_path.default.dirname(pdfAbsPath);
|
|
128
|
+
const pdfBaseName = import_node_path.default.basename(pdfAbsPath, import_node_path.default.extname(pdfAbsPath));
|
|
129
|
+
const outputDir = import_node_path.default.join(pdfDir, pdfBaseName);
|
|
130
|
+
if ((0, import_node_fs.existsSync)(outputDir)) {
|
|
131
|
+
const st = await (0, import_promises.stat)(outputDir);
|
|
132
|
+
if (!st.isDirectory()) {
|
|
133
|
+
throw new Error(`\u8F93\u51FA\u8DEF\u5F84\u5DF2\u5B58\u5728\u4F46\u4E0D\u662F\u6587\u4EF6\u5939\uFF1A${outputDir}`);
|
|
134
|
+
}
|
|
135
|
+
} else {
|
|
136
|
+
await (0, import_promises.mkdir)(outputDir, { recursive: true });
|
|
137
|
+
}
|
|
138
|
+
const pageCount = await getPdfPageCount(pdfAbsPath);
|
|
139
|
+
const selectedPages = options.pages ? parsePagesSpec(options.pages) : [];
|
|
140
|
+
const pages = selectedPages.length > 0 ? selectedPages : Array.from({ length: pageCount }, (_, i) => i + 1);
|
|
141
|
+
const invalidPage = pages.find((p) => p < 1 || p > pageCount);
|
|
142
|
+
if (invalidPage) {
|
|
143
|
+
throw new Error(`\u9875\u7801\u8D85\u51FA\u8303\u56F4\uFF1A${invalidPage}\uFF08PDF \u603B\u9875\u6570\uFF1A${pageCount}\uFF09`);
|
|
144
|
+
}
|
|
145
|
+
const width = Math.max(2, String(pages.length).length);
|
|
146
|
+
for (let i = 0; i < pages.length; i += 1) {
|
|
147
|
+
const outIndex = i + 1;
|
|
148
|
+
const fileName = `${options.name}${padNumber(outIndex, width)}.jpg`;
|
|
149
|
+
const outputPath = import_node_path.default.join(outputDir, fileName);
|
|
150
|
+
if ((0, import_node_fs.existsSync)(outputPath)) {
|
|
151
|
+
throw new Error(`\u8F93\u51FA\u6587\u4EF6\u5DF2\u5B58\u5728\uFF0C\u5DF2\u6309\u8981\u6C42\u4E2D\u6B62\uFF1A${outputPath}`);
|
|
152
|
+
}
|
|
153
|
+
const pageNumber = pages[i];
|
|
154
|
+
console.log(`\u6B63\u5728\u8F6C\u6362\uFF1A\u7B2C ${pageNumber} \u9875 -> ${fileName}`);
|
|
155
|
+
await convertSinglePageToJpg({
|
|
156
|
+
pdfPath: pdfAbsPath,
|
|
157
|
+
pageNumber,
|
|
158
|
+
dpi: options.dpi,
|
|
159
|
+
quality: options.quality,
|
|
160
|
+
outputPath
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
console.log(`\u5B8C\u6210\uFF1A\u5171\u5BFC\u51FA ${pages.length} \u5F20 JPG\uFF0C\u8F93\u51FA\u76EE\u5F55\uFF1A${outputDir}`);
|
|
164
|
+
}
|
|
165
|
+
async function main() {
|
|
166
|
+
if (process.argv.slice(2).length === 0) {
|
|
167
|
+
printChineseUsage();
|
|
168
|
+
process.exit(0);
|
|
169
|
+
}
|
|
170
|
+
const program = new import_commander.Command();
|
|
171
|
+
program.name("pdf2jpg").description("\u4ECE PDF \u4E2D\u63D0\u53D6\u9875\u9762\u5E76\u4FDD\u5B58\u4E3A JPG\uFF08\u4F9D\u8D56 ImageMagick\uFF1Amagick\uFF09").argument("<pdf>", "PDF \u6587\u4EF6\u8DEF\u5F84").option("-d, --dpi <number>", "\u5206\u8FA8\u7387\uFF08DPI\uFF09\uFF0C\u9ED8\u8BA4 150", "150").option("-q, --quality <number>", "JPG \u8D28\u91CF\uFF0C\u9ED8\u8BA4 95", "95").option("-n, --name <string>", "\u8F93\u51FA\u6587\u4EF6\u540D\u524D\u7F00\uFF0C\u9ED8\u8BA4 p", "p").option("-p, --pages <string>", "\u9875\u7801\u9009\u62E9\uFF08\u5982 10-15,20\uFF09");
|
|
172
|
+
program.parse(process.argv);
|
|
173
|
+
const inputPdf = program.args[0];
|
|
174
|
+
if (!inputPdf) {
|
|
175
|
+
printChineseUsage();
|
|
176
|
+
process.exit(1);
|
|
177
|
+
}
|
|
178
|
+
const opts = program.opts();
|
|
179
|
+
const dpi = Number(opts.dpi);
|
|
180
|
+
const quality = Number(opts.quality);
|
|
181
|
+
if (!Number.isFinite(dpi) || dpi <= 0) throw new Error("dpi \u5FC5\u987B\u662F\u5927\u4E8E 0 \u7684\u6570\u5B57\u3002");
|
|
182
|
+
if (!Number.isFinite(quality) || quality < 1 || quality > 100) {
|
|
183
|
+
throw new Error("quality \u5FC5\u987B\u662F 1-100 \u4E4B\u95F4\u7684\u6570\u5B57\u3002");
|
|
184
|
+
}
|
|
185
|
+
if (!opts.name || opts.name.trim().length === 0) throw new Error("name \u4E0D\u80FD\u4E3A\u7A7A\u3002");
|
|
186
|
+
await run(inputPdf, {
|
|
187
|
+
dpi,
|
|
188
|
+
quality,
|
|
189
|
+
name: opts.name.trim(),
|
|
190
|
+
pages: opts.pages?.trim()
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
main().catch((err) => {
|
|
194
|
+
console.error(`\u9519\u8BEF\uFF1A${err instanceof Error ? err.message : String(err)}`);
|
|
195
|
+
process.exit(1);
|
|
196
|
+
});
|
|
197
|
+
//# sourceMappingURL=cli.js.map
|
package/dist/cli.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/cli.ts"],"sourcesContent":["/**\n * pdf2jpg CLI 入口\n * 功能:使用已安装的 ImageMagick(magick)从 PDF 中提取页面并保存为 JPG。\n */\n\nimport { Command } from \"commander\";\nimport { execFile } from \"node:child_process\";\nimport { existsSync } from \"node:fs\";\nimport { mkdir, stat } from \"node:fs/promises\";\nimport path from \"node:path\";\nimport { promisify } from \"node:util\";\n\nconst execFileAsync = promisify(execFile);\n\ntype Options = {\n dpi: number;\n quality: number;\n name: string;\n pages?: string;\n};\n\n/**\n * 输出中文使用说明(无参数时)。\n * @returns {void}\n */\nfunction printChineseUsage(): void {\n const text = `\n用法:\n pdf2jpg \"文件.pdf\" [选项]\n\n说明:\n - 在 PDF 所在目录创建同名文件夹(去掉 .pdf 扩展名)\n - 将指定页面导出为 JPG,文件名规则:前缀 + 序号(从 01 开始)\n\n选项:\n -d, --dpi <数字> 分辨率(DPI),默认 150\n -q, --quality <数字> JPG 质量,默认 95\n -n, --name <前缀> 文件名前缀,默认 p(示例:p01.jpg)\n -p, --pages <范围> 选择页面:\n -p 10 第 10 页\n -p 10,12,13 第 10/12/13 页\n -p 10-15 第 10 到 15 页\n -p 10-15,20 第 10-15 页和第 20 页\n\n示例:\n pdf2jpg \"name.pdf\"\n pdf2jpg \"name.pdf\" -d 200 -q 100\n pdf2jpg \"name.pdf\" -n \"new\" -p 10-15,20\n`;\n console.log(text.trim());\n}\n\n/**\n * 将数字按指定宽度补零。\n * @param {number} n 数字\n * @param {number} width 宽度\n * @returns {string} 补零后的字符串\n */\nfunction padNumber(n: number, width: number): string {\n return String(n).padStart(width, \"0\");\n}\n\n/**\n * 解析页码参数(如:10 / 10,12,13 / 10-15 / 10-15,20)。\n * @param {string} spec 页码表达式\n * @returns {number[]} 页码数组(从 1 开始)\n */\nfunction parsePagesSpec(spec: string): number[] {\n const raw = spec\n .split(\",\")\n .map((s) => s.trim())\n .filter(Boolean);\n\n const pages: number[] = [];\n\n for (const part of raw) {\n const rangeMatch = part.match(/^(\\d+)\\s*-\\s*(\\d+)$/);\n if (rangeMatch) {\n const start = Number(rangeMatch[1]);\n const end = Number(rangeMatch[2]);\n if (!Number.isInteger(start) || !Number.isInteger(end) || start <= 0 || end <= 0) {\n throw new Error(`页码范围非法:${part}`);\n }\n if (start > end) {\n throw new Error(`页码范围起止顺序错误:${part}`);\n }\n for (let i = start; i <= end; i += 1) pages.push(i);\n continue;\n }\n\n const singleMatch = part.match(/^\\d+$/);\n if (singleMatch) {\n const v = Number(part);\n if (!Number.isInteger(v) || v <= 0) throw new Error(`页码非法:${part}`);\n pages.push(v);\n continue;\n }\n\n throw new Error(`无法解析页码参数:${part}`);\n }\n\n // 去重 + 排序(保证输出顺序稳定)\n return Array.from(new Set(pages)).sort((a, b) => a - b);\n}\n\n/**\n * 检查 magick 命令是否可用。\n * @returns {Promise<void>}\n */\nasync function ensureMagickAvailable(): Promise<void> {\n try {\n await execFileAsync(\"magick\", [\"-version\"]);\n } catch {\n throw new Error(\"未检测到 ImageMagick 的 magick 命令。请确认已安装并且在 PATH 中可用。\");\n }\n}\n\n/**\n * 获取 PDF 页数。\n * 实现:通过 `magick identify -ping file.pdf` 输出行数判断。\n * @param {string} pdfPath PDF 路径\n * @returns {Promise<number>} 页数\n */\nasync function getPdfPageCount(pdfPath: string): Promise<number> {\n const { stdout } = await execFileAsync(\"magick\", [\"identify\", \"-ping\", pdfPath], {\n maxBuffer: 10 * 1024 * 1024,\n });\n const lines = stdout\n .split(/\\r?\\n/)\n .map((s) => s.trim())\n .filter(Boolean);\n if (lines.length === 0) throw new Error(\"无法识别 PDF 页数(magick identify 输出为空)。\");\n return lines.length;\n}\n\n/**\n * 将指定 PDF 页面导出为 JPG。\n * @param {object} params 参数\n * @param {string} params.pdfPath PDF 路径\n * @param {number} params.pageNumber PDF 页码(从 1 开始)\n * @param {number} params.dpi 分辨率 DPI\n * @param {number} params.quality JPG 质量\n * @param {string} params.outputPath 输出文件路径\n * @returns {Promise<void>}\n */\nasync function convertSinglePageToJpg(params: {\n pdfPath: string;\n pageNumber: number;\n dpi: number;\n quality: number;\n outputPath: string;\n}): Promise<void> {\n const magickPageIndex = params.pageNumber - 1; // ImageMagick 以 0 为第一页索引\n await execFileAsync(\n \"magick\",\n [\n \"-density\",\n String(params.dpi),\n `${params.pdfPath}[${magickPageIndex}]`,\n \"-quality\",\n String(params.quality),\n params.outputPath,\n ],\n { maxBuffer: 10 * 1024 * 1024 }\n );\n}\n\n/**\n * 主流程:解析参数并执行转换。\n * @param {string} inputPdf 用户输入的 PDF 路径(可相对/绝对)\n * @param {Options} options 命令行选项\n * @returns {Promise<void>}\n */\nasync function run(inputPdf: string, options: Options): Promise<void> {\n if (!existsSync(inputPdf)) {\n throw new Error(`找不到文件:${inputPdf}`);\n }\n\n await ensureMagickAvailable();\n\n const pdfAbsPath = path.resolve(process.cwd(), inputPdf);\n const pdfDir = path.dirname(pdfAbsPath);\n const pdfBaseName = path.basename(pdfAbsPath, path.extname(pdfAbsPath));\n const outputDir = path.join(pdfDir, pdfBaseName);\n\n if (existsSync(outputDir)) {\n const st = await stat(outputDir);\n if (!st.isDirectory()) {\n throw new Error(`输出路径已存在但不是文件夹:${outputDir}`);\n }\n } else {\n await mkdir(outputDir, { recursive: true });\n }\n\n const pageCount = await getPdfPageCount(pdfAbsPath);\n const selectedPages = options.pages ? parsePagesSpec(options.pages) : [];\n const pages =\n selectedPages.length > 0 ? selectedPages : Array.from({ length: pageCount }, (_, i) => i + 1);\n\n const invalidPage = pages.find((p) => p < 1 || p > pageCount);\n if (invalidPage) {\n throw new Error(`页码超出范围:${invalidPage}(PDF 总页数:${pageCount})`);\n }\n\n const width = Math.max(2, String(pages.length).length);\n\n for (let i = 0; i < pages.length; i += 1) {\n const outIndex = i + 1; // 输出序号从 1 开始\n const fileName = `${options.name}${padNumber(outIndex, width)}.jpg`;\n const outputPath = path.join(outputDir, fileName);\n\n if (existsSync(outputPath)) {\n throw new Error(`输出文件已存在,已按要求中止:${outputPath}`);\n }\n\n const pageNumber = pages[i];\n console.log(`正在转换:第 ${pageNumber} 页 -> ${fileName}`);\n await convertSinglePageToJpg({\n pdfPath: pdfAbsPath,\n pageNumber,\n dpi: options.dpi,\n quality: options.quality,\n outputPath,\n });\n }\n\n console.log(`完成:共导出 ${pages.length} 张 JPG,输出目录:${outputDir}`);\n}\n\nasync function main(): Promise<void> {\n if (process.argv.slice(2).length === 0) {\n printChineseUsage();\n process.exit(0);\n }\n\n const program = new Command();\n\n program\n .name(\"pdf2jpg\")\n .description(\"从 PDF 中提取页面并保存为 JPG(依赖 ImageMagick:magick)\")\n .argument(\"<pdf>\", \"PDF 文件路径\")\n .option(\"-d, --dpi <number>\", \"分辨率(DPI),默认 150\", \"150\")\n .option(\"-q, --quality <number>\", \"JPG 质量,默认 95\", \"95\")\n .option(\"-n, --name <string>\", \"输出文件名前缀,默认 p\", \"p\")\n .option(\"-p, --pages <string>\", \"页码选择(如 10-15,20)\");\n\n program.parse(process.argv);\n\n const inputPdf = program.args[0] as string | undefined;\n if (!inputPdf) {\n printChineseUsage();\n process.exit(1);\n }\n\n const opts = program.opts<Options>();\n const dpi = Number(opts.dpi);\n const quality = Number(opts.quality);\n\n if (!Number.isFinite(dpi) || dpi <= 0) throw new Error(\"dpi 必须是大于 0 的数字。\");\n if (!Number.isFinite(quality) || quality < 1 || quality > 100) {\n throw new Error(\"quality 必须是 1-100 之间的数字。\");\n }\n if (!opts.name || opts.name.trim().length === 0) throw new Error(\"name 不能为空。\");\n\n await run(inputPdf, {\n dpi,\n quality,\n name: opts.name.trim(),\n pages: opts.pages?.trim(),\n });\n}\n\nmain().catch((err) => {\n console.error(`错误:${err instanceof Error ? err.message : String(err)}`);\n process.exit(1);\n});\n"],"mappings":"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","names":["path"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "pdf-fetch",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "使用 ImageMagick 将 PDF 页面批量导出为 JPG 的命令行工具(CLI 命令:pdf2jpg)",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"pdf",
|
|
7
|
+
"jpg",
|
|
8
|
+
"imagemagick",
|
|
9
|
+
"cli"
|
|
10
|
+
],
|
|
11
|
+
"license": "MIT",
|
|
12
|
+
"author": "",
|
|
13
|
+
"repository": {
|
|
14
|
+
"type": "git",
|
|
15
|
+
"url": "git+ssh://git@github.com/Tairraos/pdf-fetch.git"
|
|
16
|
+
},
|
|
17
|
+
"homepage": "https://github.com/Tairraos/pdf-fetch",
|
|
18
|
+
"bugs": {
|
|
19
|
+
"url": "https://github.com/Tairraos/pdf-fetch/issues"
|
|
20
|
+
},
|
|
21
|
+
"main": "dist/cli.js",
|
|
22
|
+
"bin": {
|
|
23
|
+
"pdf2jpg": "dist/cli.js"
|
|
24
|
+
},
|
|
25
|
+
"files": [
|
|
26
|
+
"dist",
|
|
27
|
+
"README.md",
|
|
28
|
+
"LICENSE"
|
|
29
|
+
],
|
|
30
|
+
"scripts": {
|
|
31
|
+
"build": "tsup",
|
|
32
|
+
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
33
|
+
"lint": "eslint .",
|
|
34
|
+
"format": "prettier -w .",
|
|
35
|
+
"prepublishOnly": "pnpm run lint && pnpm run typecheck && pnpm run build"
|
|
36
|
+
},
|
|
37
|
+
"engines": {
|
|
38
|
+
"node": ">=18"
|
|
39
|
+
},
|
|
40
|
+
"dependencies": {
|
|
41
|
+
"commander": "^12.1.0"
|
|
42
|
+
},
|
|
43
|
+
"devDependencies": {
|
|
44
|
+
"@eslint/js": "^9.20.0",
|
|
45
|
+
"@types/node": "^22.13.5",
|
|
46
|
+
"eslint": "^9.20.0",
|
|
47
|
+
"eslint-config-prettier": "^10.0.1",
|
|
48
|
+
"prettier": "^3.5.1",
|
|
49
|
+
"tsup": "^8.4.0",
|
|
50
|
+
"typescript": "^5.7.3",
|
|
51
|
+
"typescript-eslint": "^8.24.0"
|
|
52
|
+
}
|
|
53
|
+
}
|